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

添加功能:RIS自动同步

dengdx 1 месяц назад
Родитель
Сommit
66ba59f9c8

+ 424 - 0
docs/RIS_AUTO_SYNC_DESIGN.md

@@ -0,0 +1,424 @@
+# RIS自动同步功能设计文档
+
+## 📋 功能概述
+
+RIS自动同步功能用于定期从RIS系统同步患者和检查数据到本地系统,确保worklist中显示最新的检查任务信息。
+
+## 🎯 功能需求
+
+### 核心需求
+1. **自动同步机制**
+   - 根据配置的时间间隔自动触发同步
+   - 仅在worklist页面激活时启用
+   - 支持手动触发同步
+
+2. **配置管理**
+   - 从后端获取RIS配置信息
+   - 支持启用/禁用自动同步
+   - 可配置同步间隔(分钟)
+
+3. **数据同步**
+   - 调用syncRis API同步RIS数据
+   - 同步后自动刷新worklist
+
+## 🏗️ 系统架构
+
+### 1. 参与者
+
+#### 组件层级
+```
+- Pages层
+  └── src/pages/patient/worklist.tsx (主页面)
+  
+- Components层  
+  ├── src/components/RisSyncStatus.tsx (同步状态显示)
+  └── src/pages/patient/components/ActionPanel.tsx (操作面板)
+
+- Hooks层
+  └── src/hooks/useRisAutoSync.ts (自动同步Hook)
+
+- Services层
+  └── src/services/risSync/RisSyncService.ts (同步服务)
+
+- Redux层
+  └── src/states/patient/ris/risSyncSlice.ts (状态管理)
+
+- API层
+  ├── src/API/patient/risActions.ts (RIS API接口)
+  └── src/API/patient/workActions.ts (工作列表API)
+```
+
+#### 类和方法
+```typescript
+// RisSyncService类
+- startAutoSync(): 启动自动同步
+- stopAutoSync(): 停止自动同步  
+- syncOnce(): 执行单次同步
+- updateConfig(): 更新配置
+
+// useRisAutoSync Hook
+- 管理同步生命周期
+- 处理页面切换逻辑
+
+// risSyncSlice
+- setSyncConfig: 设置同步配置
+- setSyncStatus: 设置同步状态
+- setLastSyncTime: 设置最后同步时间
+- setSyncError: 设置同步错误
+```
+
+## 🔄 交互流程
+
+```mermaid
+sequenceDiagram
+    participant User
+    participant Worklist
+    participant Hook as useRisAutoSync
+    participant Service as RisSyncService
+    participant API
+    participant Redux
+    
+    User->>Worklist: 进入worklist页面
+    Worklist->>Hook: 初始化自动同步
+    Hook->>API: getRisConfig()
+    API-->>Hook: 返回配置
+    Hook->>Redux: 保存配置
+    
+    alt 自动同步启用
+        Hook->>Service: startAutoSync(interval)
+        loop 每N分钟
+            Service->>API: syncRis()
+            API-->>Service: 同步结果
+            Service->>API: fetchTaskList()
+            API-->>Service: 任务列表
+            Service->>Redux: 更新数据
+            Redux-->>Worklist: 刷新显示
+        end
+    end
+    
+    User->>Worklist: 手动同步
+    Worklist->>Service: syncOnce()
+    Service->>API: syncRis()
+    API-->>Service: 同步结果
+    Service->>API: fetchTaskList()
+    API-->>Service: 任务列表
+    Service->>Redux: 更新数据
+    
+    User->>Worklist: 离开页面
+    Worklist->>Hook: 清理
+    Hook->>Service: stopAutoSync()
+```
+
+## 📊 数据流
+
+```mermaid
+graph TD
+    A[RIS系统] -->|API调用| B[syncRis接口]
+    B --> C[同步数据到本地]
+    C --> D[fetchTaskList接口]
+    D --> E[获取混合数据]
+    E --> F[Redux Store更新]
+    F --> G[Worklist组件]
+    G --> H[UI展示]
+    
+    I[配置API] --> J[getRisConfig]
+    J --> K[Redux配置状态]
+    K --> L[自动同步控制]
+    L --> B
+```
+
+## 🗄️ 数据结构
+
+### RIS同步状态
+```typescript
+interface RisSyncState {
+  // 配置信息
+  config: {
+    mwl_enable: boolean;           // RIS是否启用
+    mwl_refresh_enable: boolean;   // 自动刷新是否启用
+    mwl_refresh_interval: number;  // 刷新间隔(分钟)
+  } | null;
+  
+  // 同步状态
+  syncStatus: 'idle' | 'syncing' | 'success' | 'error';
+  lastSyncTime: string | null;     // 最后同步时间
+  nextSyncTime: string | null;     // 下次同步时间
+  syncCount: number;                // 同步条目数
+  
+  // 错误信息
+  error: {
+    message: string;
+    timestamp: string;
+  } | null;
+  
+  // 控制状态
+  isAutoSyncActive: boolean;        // 自动同步是否激活
+  isManuallySyncing: boolean;       // 是否正在手动同步
+}
+```
+
+### 混合任务数据
+```typescript
+interface Task {
+  // 基础字段
+  StudyID: string;
+  PatientID: string;
+  PatientName: string;
+  AccessionNumber: string;
+  
+  // RIS特有字段
+  entry_id?: string;               // RIS条目ID
+  scheduled?: {                    // 调度信息
+    scheduled_ae_title: string;
+    scheduled_performing_physician_name: string;
+    scheduled_procedure_step_id: string;
+  };
+  protocol_code?: Array<{          // 协议代码
+    code_value: string;
+    code_meaning: string;
+  }>;
+  
+  // 其他字段...
+}
+```
+
+## 🚀 执行流程
+
+### 起点:用户进入worklist页面
+
+```mermaid
+flowchart TD
+    Start[用户进入worklist页面] --> Check{检查currentKey}
+    Check -->|是worklist| InitSync[初始化同步]
+    Check -->|否| NoSync[不启动同步]
+    
+    InitSync --> GetConfig[获取RIS配置]
+    GetConfig --> CheckEnable{检查是否启用}
+    CheckEnable -->|启用| SetupTimer[设置定时器]
+    CheckEnable -->|禁用| ShowDisabled[显示禁用状态]
+    
+    SetupTimer --> FirstSync[立即执行首次同步]
+    FirstSync --> WaitInterval[等待间隔时间]
+    WaitInterval --> AutoSync[执行自动同步]
+    AutoSync --> UpdateUI[更新界面]
+    UpdateUI --> WaitInterval
+    
+    ShowDisabled --> End[结束]
+    NoSync --> End
+```
+
+### 同步执行流程
+
+```mermaid
+flowchart TD
+    SyncStart[开始同步] --> SetStatus[设置同步中状态]
+    SetStatus --> GetTimeRange[获取时间范围]
+    GetTimeRange --> CallSyncAPI[调用syncRis API]
+    
+    CallSyncAPI -->|成功| SaveCount[保存同步数量]
+    CallSyncAPI -->|失败| HandleError[处理错误]
+    
+    SaveCount --> FetchList[调用fetchTaskList]
+    FetchList -->|成功| UpdateStore[更新Redux Store]
+    FetchList -->|失败| HandleError
+    
+    UpdateStore --> SetSuccess[设置成功状态]
+    HandleError --> SetError[设置错误状态]
+    
+    SetSuccess --> UpdateTime[更新同步时间]
+    SetError --> LogError[记录错误日志]
+    
+    UpdateTime --> SyncEnd[同步结束]
+    LogError --> SyncEnd
+```
+
+## 🧪 测试方案
+
+### 1. 功能测试场景
+
+#### 场景1:自动同步启用测试
+```
+前置条件:
+- RIS配置已启用自动同步
+- 同步间隔设置为1分钟
+
+测试步骤:
+1. 进入worklist页面
+2. 观察是否立即触发首次同步
+3. 等待1分钟
+4. 验证是否自动触发下次同步
+
+预期结果:
+- 首次同步成功执行
+- 每分钟自动同步一次
+- UI显示最新同步时间
+```
+
+#### 场景2:手动同步测试
+```
+测试步骤:
+1. 进入worklist页面
+2. 点击手动同步按钮
+3. 观察同步状态变化
+
+预期结果:
+- 按钮显示加载状态
+- 同步完成后刷新列表
+- 显示同步成功提示
+```
+
+#### 场景3:页面切换测试
+```
+测试步骤:
+1. 在worklist页面启动自动同步
+2. 切换到其他页面
+3. 等待同步间隔时间
+4. 返回worklist页面
+
+预期结果:
+- 离开页面时停止同步
+- 其他页面不触发同步
+- 返回时重新启动同步
+```
+
+### 2. 边界测试
+
+- **网络异常**:模拟网络断开,验证错误处理
+- **配置异常**:RIS未启用时的行为
+- **并发同步**:手动和自动同步冲突处理
+- **长时间运行**:验证内存泄漏和定时器清理
+
+## 🐛 潜在问题及解决方案
+
+### 1. 内存泄漏风险
+**问题**:定时器未正确清理
+**解决**:
+```typescript
+useEffect(() => {
+  const timer = setInterval(sync, interval);
+  return () => clearInterval(timer); // 清理定时器
+}, []);
+```
+
+### 2. 并发同步冲突
+**问题**:手动和自动同步同时触发
+**解决**:使用状态锁防止并发
+```typescript
+if (isSyncing) {
+  message.warning('正在同步中,请稍后再试');
+  return;
+}
+```
+
+### 3. 页面切换状态丢失
+**问题**:切换页面后同步状态重置
+**解决**:将同步状态存储在Redux中持久化
+
+### 4. 同步失败重试
+**问题**:单次失败导致长时间无数据
+**解决**:实现指数退避重试机制
+```typescript
+const retryWithBackoff = async (fn, maxRetries = 3) => {
+  for (let i = 0; i < maxRetries; i++) {
+    try {
+      return await fn();
+    } catch (error) {
+      if (i === maxRetries - 1) throw error;
+      await sleep(Math.pow(2, i) * 1000);
+    }
+  }
+};
+```
+
+## 📝 实施步骤
+
+1. **创建基础结构** (Day 1)
+   - 创建risSyncSlice
+   - 创建RisSyncService类
+   - 创建useRisAutoSync Hook
+
+2. **集成到worklist** (Day 2)
+   - 修改worklist.tsx集成Hook
+   - 添加同步状态UI组件
+   - 添加手动同步按钮
+
+3. **完善功能** (Day 3)
+   - 实现错误处理
+   - 添加重试机制
+   - 优化性能
+
+4. **测试验证** (Day 4)
+   - 单元测试
+   - 集成测试
+   - 用户验收测试
+
+## 🔧 配置示例
+
+```typescript
+// 默认配置
+const DEFAULT_CONFIG = {
+  mwl_enable: true,
+  mwl_refresh_enable: true,
+  mwl_refresh_interval: 5, // 默认5分钟
+};
+
+// 同步时间范围(当天)
+const getSyncTimeRange = () => {
+  const now = new Date();
+  const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+  const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
+  
+  return {
+    start_time: startOfDay.toISOString().replace('Z', '+08:00'),
+    end_time: endOfDay.toISOString().replace('Z', '+08:00'),
+  };
+};
+```
+
+## 📚 依赖关系
+
+```mermaid
+graph LR
+    A[worklist.tsx] --> B[useRisAutoSync]
+    B --> C[RisSyncService]
+    C --> D[risActions API]
+    C --> E[workActions API]
+    B --> F[risSyncSlice]
+    F --> G[Redux Store]
+    A --> H[RisSyncStatus组件]
+    H --> F
+```
+
+## 🎨 UI设计
+
+### 同步状态显示
+- 位置:worklist页面顶部工具栏
+- 显示内容:
+  - 同步状态图标(旋转/静止)
+  - 最后同步时间
+  - 下次同步倒计时
+  - 手动同步按钮
+
+### 错误提示
+- 使用Antd Message组件
+- 显示错误原因和重试建议
+
+## 📈 性能优化
+
+1. **防抖处理**:避免频繁触发同步
+2. **缓存机制**:缓存配置信息减少API调用
+3. **批量更新**:合并多个状态更新减少渲染
+4. **懒加载**:仅在需要时加载同步服务
+
+## 🔒 安全考虑
+
+1. **权限验证**:确保用户有同步权限
+2. **数据验证**:验证同步数据的完整性
+3. **错误隔离**:同步失败不影响其他功能
+4. **日志记录**:记录关键操作用于审计
+
+---
+
+*文档版本:1.0.0*
+*更新日期:2025-01-31*
+*作者:系统架构师*

+ 407 - 0
docs/RIS_AUTO_SYNC_FINAL.md

@@ -0,0 +1,407 @@
+# RIS自动同步功能 - 最终实现方案
+
+## 📋 功能概述
+
+RIS自动同步功能在后台静默运行,定期从RIS系统同步数据到本地,确保worklist显示最新信息。
+
+## 🎯 核心需求
+
+1. **自动同步机制**
+   - worklist组件挂载时启动自动同步
+   - worklist组件卸载时停止自动同步
+   - 根据配置的时间间隔自动触发
+
+2. **配置管理**
+   - 在程序启动时获取RIS配置
+   - 配置存储在全局Redux state中
+   - worklist从全局state读取配置
+
+3. **状态反馈**
+   - 使用Antd Message弹框显示同步结果
+   - 成功/失败信息自动消失
+   - 不显示固定的UI组件
+
+## 🏗️ 实现架构
+
+### 文件结构
+
+```
+src/
+├── states/
+│   └── ris/
+│       └── risSyncSlice.ts          # RIS配置和状态管理
+├── hooks/
+│   └── useRisAutoSync.ts            # 自动同步Hook
+├── services/
+│   └── risSync/
+│       └── RisSyncService.ts        # 同步服务类
+├── pages/
+│   ├── index/
+│   │   └── AppInitializer.tsx       # [修改] 启动时获取配置
+│   └── patient/
+│       └── worklist.tsx             # [修改] 集成自动同步
+└── states/
+    └── store.ts                      # [修改] 注册reducer
+```
+
+## 🔄 执行流程
+
+### 1. 程序启动流程
+
+```mermaid
+sequenceDiagram
+    participant App as 应用启动
+    participant Init as AppInitializer
+    participant API
+    participant Redux
+    
+    App->>Init: 初始化应用
+    Init->>API: getRisConfig()
+    API-->>Init: RIS配置
+    Init->>Redux: 保存配置到全局state
+    Redux-->>Init: 配置保存成功
+```
+
+### 2. Worklist自动同步流程
+
+```mermaid
+sequenceDiagram
+    participant User
+    participant Worklist
+    participant Hook as useRisAutoSync
+    participant Redux
+    participant Service as RisSyncService
+    participant API
+    participant Message
+    
+    User->>Worklist: 进入worklist页面
+    Worklist->>Hook: 组件挂载,启动Hook
+    Hook->>Redux: 读取RIS配置
+    Redux-->>Hook: 返回配置
+    
+    alt 自动同步启用
+        Hook->>Service: 启动定时器
+        loop 每N分钟
+            Service->>API: syncRis()
+            API-->>Service: 同步结果
+            Service->>API: fetchTaskList()
+            API-->>Service: 任务列表
+            Service->>Redux: 更新worklist数据
+            Service->>Message: 显示同步结果
+            Note over Message: 3秒后自动消失
+        end
+    end
+    
+    User->>Worklist: 离开worklist页面
+    Worklist->>Hook: 组件卸载
+    Hook->>Service: 清理定时器
+```
+
+## 📊 数据结构
+
+### Redux State
+
+```typescript
+// RIS同步状态(精简版)
+interface RisSyncState {
+  // 配置信息(程序启动时获取)
+  config: {
+    mwl_enable: boolean;           
+    mwl_refresh_enable: boolean;   
+    mwl_refresh_interval: number;  
+  } | null;
+  
+  // 运行时状态(不需要UI显示)
+  isRunning: boolean;              // 同步是否正在运行
+  lastSyncTime: string | null;     // 最后同步时间
+}
+```
+
+## 💻 代码实现
+
+### 1. risSyncSlice.ts
+
+```typescript
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { getRisConfig, RisConfigData } from '@/API/patient/risActions';
+
+interface RisSyncState {
+  config: RisConfigData | null;
+  isRunning: boolean;
+  lastSyncTime: string | null;
+}
+
+const initialState: RisSyncState = {
+  config: null,
+  isRunning: false,
+  lastSyncTime: null,
+};
+
+// 获取配置的thunk(程序启动时调用)
+export const fetchRisConfigThunk = createAsyncThunk(
+  'risSync/fetchConfig',
+  async () => {
+    const response = await getRisConfig();
+    return response.data;
+  }
+);
+
+const risSyncSlice = createSlice({
+  name: 'risSync',
+  initialState,
+  reducers: {
+    setRunning: (state, action: PayloadAction<boolean>) => {
+      state.isRunning = action.payload;
+    },
+    setLastSyncTime: (state, action: PayloadAction<string>) => {
+      state.lastSyncTime = action.payload;
+    },
+  },
+  extraReducers: (builder) => {
+    builder.addCase(fetchRisConfigThunk.fulfilled, (state, action) => {
+      state.config = action.payload;
+      console.log('[RIS] 配置加载成功:', action.payload);
+    });
+  },
+});
+
+export const { setRunning, setLastSyncTime } = risSyncSlice.actions;
+export default risSyncSlice.reducer;
+```
+
+### 2. AppInitializer.tsx 修改
+
+```typescript
+// 在现有的 AppInitializer 中添加
+import { fetchRisConfigThunk } from '@/states/ris/risSyncSlice';
+
+// 在 useEffect 中添加
+useEffect(() => {
+  const fetchData = async () => {
+    // ... 现有的初始化代码
+    
+    // 获取RIS配置
+    try {
+      await dispatch(fetchRisConfigThunk());
+      console.log('[AppInitializer] RIS配置加载完成');
+    } catch (error) {
+      console.error('[AppInitializer] RIS配置加载失败:', error);
+      // 配置加载失败不影响应用启动
+    }
+  };
+  
+  fetchData();
+}, [dispatch]);
+```
+
+### 3. useRisAutoSync.ts
+
+```typescript
+import { useEffect, useRef } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { message } from 'antd';
+import { RootState, AppDispatch } from '@/states/store';
+import { setRunning, setLastSyncTime } from '@/states/ris/risSyncSlice';
+import { RisSyncService } from '@/services/risSync/RisSyncService';
+import { fetchWorkThunk } from '@/states/patient/worklist/slices/workSlice';
+
+export const useRisAutoSync = () => {
+  const dispatch = useDispatch<AppDispatch>();
+  const config = useSelector((state: RootState) => state.risSync.config);
+  const filters = useSelector((state: RootState) => state.workFilters);
+  const page = useSelector((state: RootState) => state.workPagination.page);
+  const pageSize = useSelector((state: RootState) => state.workPagination.pageSize);
+  
+  const serviceRef = useRef<RisSyncService | null>(null);
+
+  useEffect(() => {
+    // 检查是否应该启动同步
+    if (!config?.mwl_enable || !config?.mwl_refresh_enable) {
+      console.log('[useRisAutoSync] RIS自动同步未启用');
+      return;
+    }
+
+    console.log('[useRisAutoSync] 启动RIS自动同步,间隔:', config.mwl_refresh_interval, '分钟');
+    
+    // 创建服务实例
+    serviceRef.current = new RisSyncService({
+      interval: config.mwl_refresh_interval * 60 * 1000, // 转换为毫秒
+      onSyncComplete: (success, count) => {
+        if (success) {
+          message.success(`RIS同步成功,同步了 ${count} 条数据`, 3);
+          dispatch(setLastSyncTime(new Date().toISOString()));
+          // 刷新worklist
+          dispatch(fetchWorkThunk({ page, pageSize, filters }));
+        } else {
+          // 错误只在控制台显示,不打扰用户
+          console.error('[useRisAutoSync] 同步失败');
+        }
+      },
+    });
+
+    // 启动同步
+    serviceRef.current.start();
+    dispatch(setRunning(true));
+
+    // 清理函数
+    return () => {
+      console.log('[useRisAutoSync] 停止RIS自动同步');
+      if (serviceRef.current) {
+        serviceRef.current.stop();
+        serviceRef.current = null;
+      }
+      dispatch(setRunning(false));
+    };
+  }, [config, dispatch, page, pageSize, filters]);
+};
+```
+
+### 4. RisSyncService.ts
+
+```typescript
+import { syncRis, RisTimeUtils } from '@/API/patient/risActions';
+
+interface RisSyncServiceOptions {
+  interval: number;
+  onSyncComplete?: (success: boolean, count: number) => void;
+}
+
+export class RisSyncService {
+  private timer: NodeJS.Timeout | null = null;
+  private isRunning = false;
+  private options: RisSyncServiceOptions;
+
+  constructor(options: RisSyncServiceOptions) {
+    this.options = options;
+  }
+
+  async start() {
+    if (this.isRunning) {
+      console.log('[RisSyncService] 服务已在运行');
+      return;
+    }
+
+    this.isRunning = true;
+    
+    // 立即执行一次同步
+    await this.performSync();
+    
+    // 设置定时器
+    this.timer = setInterval(() => {
+      this.performSync();
+    }, this.options.interval);
+    
+    console.log('[RisSyncService] 自动同步已启动');
+  }
+
+  stop() {
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
+    this.isRunning = false;
+    console.log('[RisSyncService] 自动同步已停止');
+  }
+
+  private async performSync() {
+    try {
+      console.log('[RisSyncService] 开始同步RIS数据...');
+      
+      // 获取今天的时间范围
+      const timeRange = RisTimeUtils.getTodayRange();
+      
+      // 执行同步
+      const result = await syncRis({
+        start_time: timeRange.start,
+        end_time: timeRange.end,
+      });
+      
+      console.log(`[RisSyncService] 同步成功,共同步 ${result.data.count} 条数据`);
+      
+      // 回调通知
+      if (this.options.onSyncComplete) {
+        this.options.onSyncComplete(true, result.data.count);
+      }
+    } catch (error) {
+      console.error('[RisSyncService] 同步失败:', error);
+      
+      // 回调通知
+      if (this.options.onSyncComplete) {
+        this.options.onSyncComplete(false, 0);
+      }
+    }
+  }
+}
+```
+
+### 5. worklist.tsx 修改
+
+```typescript
+// 在 import 部分添加
+import { useRisAutoSync } from '@/hooks/useRisAutoSync';
+
+// 在组件内部添加(其他代码保持不变)
+const WorklistPage: React.FC = () => {
+  // ... 现有的代码
+  
+  // 启用RIS自动同步
+  useRisAutoSync();
+  
+  // ... 现有的代码
+};
+```
+
+### 6. store.ts 修改
+
+```typescript
+// 添加 import
+import risSyncReducer from './ris/risSyncSlice';
+
+// 在 reducer 对象中添加
+const store = configureStore({
+  reducer: {
+    // ... 现有的 reducers
+    risSync: risSyncReducer,
+  },
+  // ... 其他配置
+});
+```
+
+## 🧪 测试步骤
+
+### 1. 验证配置加载
+```
+1. 启动应用
+2. 打开浏览器控制台
+3. 查看是否有 "[RIS] 配置加载成功" 日志
+```
+
+### 2. 验证自动同步
+```
+1. 进入worklist页面
+2. 查看控制台是否有 "[useRisAutoSync] 启动RIS自动同步" 日志
+3. 等待配置的时间间隔
+4. 观察是否出现同步成功的消息弹框
+5. 验证列表是否刷新
+```
+
+### 3. 验证生命周期控制
+```
+1. 在worklist页面等待同步启动
+2. 切换到其他页面
+3. 查看控制台是否有 "[useRisAutoSync] 停止RIS自动同步" 日志
+4. 返回worklist页面
+5. 验证同步重新启动
+```
+
+## 📝 注意事项
+
+1. **性能优化**:同步操作不会阻塞UI
+2. **错误处理**:同步失败只在控制台记录,不干扰用户
+3. **内存管理**:组件卸载时正确清理定时器
+4. **配置缓存**:配置在应用启动时加载一次,避免重复请求
+
+---
+
+*文档版本:2.0.0*
+*更新日期:2025-01-31*

+ 146 - 0
src/hooks/useRisAutoSync.ts

@@ -0,0 +1,146 @@
+import { useEffect, useRef } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { message } from 'antd';
+import { RootState, AppDispatch } from '@/states/store';
+import { setRunning, setLastSyncTime, setSyncing } from '@/states/ris/risSyncSlice';
+import { RisSyncService } from '@/services/risSync/RisSyncService';
+import { fetchWorkThunk } from '@/states/patient/worklist/slices/workSlice';
+
+/**
+ * RIS自动同步Hook
+ * 在worklist组件中使用,负责管理RIS数据的自动同步
+ */
+export const useRisAutoSync = () => {
+  const dispatch = useDispatch<AppDispatch>();
+  const config = useSelector((state: RootState) => state.risSync?.config);
+  const isSyncing = useSelector((state: RootState) => state.risSync?.isSyncing);
+  const filters = useSelector((state: RootState) => state.workFilters);
+  const page = useSelector((state: RootState) => state.workPagination.page);
+  const pageSize = useSelector((state: RootState) => state.workPagination.pageSize);
+  
+  const serviceRef = useRef<RisSyncService | null>(null);
+  const messageDestroyRef = useRef<(() => void) | null>(null);
+
+  useEffect(() => {
+    // 检查配置是否已加载
+    if (!config) {
+      console.log('[useRisAutoSync] RIS配置尚未加载,等待配置...');
+      return;
+    }
+
+    // 检查是否应该启动同步
+    if (!config.mwl_enable) {
+      console.log('[useRisAutoSync] RIS功能未启用 (mwl_enable=false)');
+      return;
+    }
+
+    if (!config.mwl_refresh_enable) {
+      console.log('[useRisAutoSync] RIS自动刷新未启用 (mwl_refresh_enable=false)');
+      return;
+    }
+
+    // 检查同步间隔配置
+    const intervalMinutes = config.mwl_refresh_interval;
+    if (!intervalMinutes || intervalMinutes <= 0) {
+      console.error('[useRisAutoSync] 无效的同步间隔配置:', intervalMinutes);
+      return;
+    }
+
+    console.log('[useRisAutoSync] ==========================================');
+    console.log('[useRisAutoSync] RIS自动同步配置:');
+    console.log('[useRisAutoSync]   - RIS启用:', config.mwl_enable);
+    console.log('[useRisAutoSync]   - 自动刷新:', config.mwl_refresh_enable);
+    console.log('[useRisAutoSync]   - 同步间隔:', intervalMinutes, '分钟');
+    console.log('[useRisAutoSync] ==========================================');
+    
+    // 创建服务实例
+    serviceRef.current = new RisSyncService({
+      interval: intervalMinutes * 60 * 1000, // 转换为毫秒
+      onSyncStart: () => {
+        // 设置同步中状态,防止重复同步
+        if (isSyncing) {
+          console.log('[useRisAutoSync] 正在同步中,跳过本次同步');
+          return;
+        }
+        dispatch(setSyncing(true));
+        
+        // 显示同步中的提示,保存destroy函数
+        const hide = message.loading('正在同步RIS数据...', 0);
+        messageDestroyRef.current = hide;
+      },
+      onSyncComplete: (success, count) => {
+        // 关闭加载提示
+        if (messageDestroyRef.current) {
+          messageDestroyRef.current();
+          messageDestroyRef.current = null;
+        }
+
+        // 更新同步状态
+        dispatch(setSyncing(false));
+        
+        if (success) {
+          // 显示成功消息
+          if (count > 0) {
+            message.success(`RIS同步成功,同步了 ${count} 条数据`, 3);
+          } else {
+            message.info('RIS同步完成,暂无新数据', 2);
+          }
+          
+          // 更新最后同步时间
+          const now = new Date();
+          const timeStr = now.toLocaleTimeString('zh-CN');
+          dispatch(setLastSyncTime(now.toISOString()));
+          console.log(`[useRisAutoSync] 同步成功,时间: ${timeStr},数量: ${count}`);
+          
+          // 刷新worklist数据
+          console.log('[useRisAutoSync] 刷新worklist数据...');
+          dispatch(fetchWorkThunk({ 
+            page, 
+            pageSize, 
+            filters 
+          })).then(() => {
+            console.log('[useRisAutoSync] Worklist数据刷新完成');
+          }).catch(err => {
+            console.error('[useRisAutoSync] Worklist数据刷新失败:', err);
+          });
+        } else {
+          // 同步失败,只在控制台记录,不显示错误消息避免打扰用户
+          console.error('[useRisAutoSync] 同步失败,将在下次定时继续尝试');
+        }
+      },
+    });
+
+    // 启动同步服务
+    console.log('[useRisAutoSync] 启动RIS自动同步服务...');
+    serviceRef.current.start();
+    dispatch(setRunning(true));
+
+    // 清理函数 - 组件卸载时执行
+    return () => {
+      console.log('[useRisAutoSync] 组件卸载,停止RIS自动同步服务');
+      
+      // 关闭所有消息提示
+      if (messageDestroyRef.current) {
+        messageDestroyRef.current();
+        messageDestroyRef.current = null;
+      }
+      
+      // 停止服务
+      if (serviceRef.current) {
+        serviceRef.current.stop();
+        serviceRef.current = null;
+      }
+      
+      // 更新运行状态
+      dispatch(setRunning(false));
+      dispatch(setSyncing(false));
+    };
+  }, [config, dispatch, page, pageSize, filters]); // 依赖项包含配置和分页参数
+
+  // 返回同步状态,供组件使用(虽然当前不需要显示UI)
+  return {
+    isRunning: useSelector((state: RootState) => state.risSync?.isRunning || false),
+    lastSyncTime: useSelector((state: RootState) => state.risSync?.lastSyncTime),
+    isSyncing: isSyncing || false,
+  };
+};

+ 10 - 0
src/pages/index/AppInitializer.tsx

@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
 import { getPatientTypes } from '@/states/patientTypeSlice';
 import { getBodyParts } from '@/states/bodyPartSlice';
 import { initializeProductState } from '@/states/productSlice';
+import { fetchRisConfigThunk } from '@/states/ris/risSyncSlice';
 import type { RootState, AppDispatch } from '@/states/store';
 
 interface AppInitializerProps {
@@ -23,6 +24,15 @@ const AppInitializer: React.FC<AppInitializerProps> = ({ onInitialized }) => {
           })
         );
         await dispatch(initializeProductState());
+        
+        // 获取RIS配置(配置加载失败不影响应用启动)
+        try {
+          await dispatch(fetchRisConfigThunk()).unwrap();
+          console.log('[AppInitializer] RIS配置加载完成');
+        } catch (error) {
+          console.error('[AppInitializer] RIS配置加载失败:', error);
+          // 配置加载失败不影响应用启动,继续执行
+        }
       }
       onInitialized();
     };

+ 4 - 0
src/pages/patient/worklist.tsx

@@ -3,6 +3,7 @@ import { Row, Col, Button, Drawer, Grid } from 'antd';
 import { SettingOutlined } from '@ant-design/icons';
 import { FormattedMessage } from 'react-intl';
 import { useSelector, useDispatch } from 'react-redux';
+import { useRisAutoSync } from '@/hooks/useRisAutoSync';
 import {
   fetchWorkThunk,
   workSelectionSlice,
@@ -30,6 +31,9 @@ const WorklistPage: React.FC = () => {
   const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]); // 新增:列配置状态
   const [selectedPatientForPortrait, setSelectedPatientForPortrait] = useState<Task | null>(null); // 照片显示用的选中患者
 
+  // 启用RIS自动同步(在后台静默运行)
+  useRisAutoSync();
+
   const dispatch: AppDispatch = useDispatch();
   const filters = useSelector((state: RootState) => state.workFilters);
   const page = useSelector((state: RootState) => state.workPagination.page);

+ 152 - 0
src/services/risSync/RisSyncService.ts

@@ -0,0 +1,152 @@
+import { syncRis, RisTimeUtils, RisSyncRequest } from '@/API/patient/risActions';
+
+interface RisSyncServiceOptions {
+  interval: number;  // 同步间隔(毫秒)
+  onSyncComplete?: (success: boolean, count: number) => void;
+  onSyncStart?: () => void;
+}
+
+export class RisSyncService {
+  private timer: NodeJS.Timeout | null = null;
+  private isRunning = false;
+  private options: RisSyncServiceOptions;
+  private retryCount = 0;
+  private readonly maxRetries = 3;
+
+  constructor(options: RisSyncServiceOptions) {
+    this.options = options;
+  }
+
+  /**
+   * 启动自动同步
+   */
+  async start() {
+    if (this.isRunning) {
+      console.log('[RisSyncService] 服务已在运行,忽略重复启动');
+      return;
+    }
+
+    this.isRunning = true;
+    console.log('[RisSyncService] 自动同步服务启动');
+    
+    // 立即执行一次同步
+    await this.performSync();
+    
+    // 设置定时器
+    this.timer = setInterval(() => {
+      this.performSync();
+    }, this.options.interval);
+    
+    console.log(`[RisSyncService] 定时器已设置,间隔: ${this.options.interval}ms (${this.options.interval / 60000}分钟)`);
+  }
+
+  /**
+   * 停止自动同步
+   */
+  stop() {
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
+    this.isRunning = false;
+    this.retryCount = 0;
+    console.log('[RisSyncService] 自动同步服务已停止');
+  }
+
+  /**
+   * 执行一次同步
+   */
+  private async performSync() {
+    try {
+      console.log('[RisSyncService] ========== 开始同步RIS数据 ==========');
+      
+      // 通知开始同步
+      if (this.options.onSyncStart) {
+        this.options.onSyncStart();
+      }
+      
+      // 获取今天的时间范围
+      const timeRange = RisTimeUtils.getTodayRange();
+      console.log('[RisSyncService] 同步时间范围:', {
+        start: timeRange.start,
+        end: timeRange.end
+      });
+      
+      // 构建同步请求参数
+      const syncParams: RisSyncRequest = {
+        start_time: timeRange.start,
+        end_time: timeRange.end,
+      };
+      
+      // 执行同步
+      const result = await syncRis(syncParams);
+      
+      console.log(`[RisSyncService] ✅ 同步成功,共同步 ${result.data.count} 条数据`);
+      
+      // 重置重试计数
+      this.retryCount = 0;
+      
+      // 回调通知
+      if (this.options.onSyncComplete) {
+        this.options.onSyncComplete(true, result.data.count);
+      }
+      
+      console.log('[RisSyncService] ========== 同步完成 ==========');
+    } catch (error) {
+      console.error('[RisSyncService] ❌ 同步失败:', error);
+      
+      // 错误重试逻辑
+      this.handleSyncError(error);
+    }
+  }
+
+  /**
+   * 处理同步错误
+   */
+  private async handleSyncError(error: any) {
+    this.retryCount++;
+    
+    if (this.retryCount <= this.maxRetries) {
+      const retryDelay = this.calculateRetryDelay(this.retryCount);
+      console.log(`[RisSyncService] 将在 ${retryDelay / 1000} 秒后重试 (第 ${this.retryCount}/${this.maxRetries} 次)`);
+      
+      // 延迟后重试
+      setTimeout(() => {
+        if (this.isRunning) {
+          this.performSync();
+        }
+      }, retryDelay);
+    } else {
+      console.error('[RisSyncService] 已达到最大重试次数,等待下次定时同步');
+      this.retryCount = 0;
+      
+      // 回调通知失败
+      if (this.options.onSyncComplete) {
+        this.options.onSyncComplete(false, 0);
+      }
+    }
+  }
+
+  /**
+   * 计算重试延迟(指数退避)
+   */
+  private calculateRetryDelay(retryCount: number): number {
+    // 基础延迟5秒,每次重试翻倍,最大延迟30秒
+    const baseDelay = 5000;
+    const maxDelay = 30000;
+    const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), maxDelay);
+    return delay;
+  }
+
+  /**
+   * 获取服务状态
+   */
+  getStatus() {
+    return {
+      isRunning: this.isRunning,
+      hasTimer: this.timer !== null,
+      interval: this.options.interval,
+      retryCount: this.retryCount,
+    };
+  }
+}

+ 63 - 0
src/states/ris/risSyncSlice.ts

@@ -0,0 +1,63 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { getRisConfig, RisConfigData } from '@/API/patient/risActions';
+
+interface RisSyncState {
+  config: RisConfigData | null;
+  isRunning: boolean;
+  lastSyncTime: string | null;
+  isSyncing: boolean; // 添加同步中标志,防止重复同步
+}
+
+const initialState: RisSyncState = {
+  config: null,
+  isRunning: false,
+  lastSyncTime: null,
+  isSyncing: false,
+};
+
+// 获取配置的thunk(程序启动时调用)
+export const fetchRisConfigThunk = createAsyncThunk(
+  'risSync/fetchConfig',
+  async () => {
+    console.log('[RIS] 开始加载RIS配置...');
+    const response = await getRisConfig();
+    return response.data;
+  }
+);
+
+const risSyncSlice = createSlice({
+  name: 'risSync',
+  initialState,
+  reducers: {
+    setRunning: (state, action: PayloadAction<boolean>) => {
+      state.isRunning = action.payload;
+    },
+    setLastSyncTime: (state, action: PayloadAction<string>) => {
+      state.lastSyncTime = action.payload;
+    },
+    setSyncing: (state, action: PayloadAction<boolean>) => {
+      state.isSyncing = action.payload;
+    },
+  },
+  extraReducers: (builder) => {
+    builder
+      .addCase(fetchRisConfigThunk.pending, (state) => {
+        console.log('[RIS] 正在加载配置...');
+      })
+      .addCase(fetchRisConfigThunk.fulfilled, (state, action) => {
+        state.config = action.payload;
+        console.log('[RIS] 配置加载成功:', {
+          mwl_enable: action.payload.mwl_enable,
+          mwl_refresh_enable: action.payload.mwl_refresh_enable,
+          mwl_refresh_interval: action.payload.mwl_refresh_interval,
+        });
+      })
+      .addCase(fetchRisConfigThunk.rejected, (state, action) => {
+        console.error('[RIS] 配置加载失败:', action.error.message);
+        // 配置加载失败不影响应用运行
+      });
+  },
+});
+
+export const { setRunning, setLastSyncTime, setSyncing } = risSyncSlice.actions;
+export default risSyncSlice.reducer;

+ 2 - 0
src/states/store.ts

@@ -86,6 +86,7 @@ import {
 import binDiskInfoSlice from './patient/bin/slices/binDiskInfoSlice';
 import dicomOverlayReducer from './view/dicomOverlaySlice';
 import printReducer from './print/printSlice';
+import risSyncReducer from './ris/risSyncSlice';
 
 const store = configureStore({
   reducer: {
@@ -162,6 +163,7 @@ const store = configureStore({
     binDiskInfo: binDiskInfoSlice.reducer,
     dicomOverlay: dicomOverlayReducer,
     print: printReducer,
+    risSync: risSyncReducer,
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware().concat(