传输队列状态管理实现.md 11 KB

传输队列状态管理实现文档

概述

本文档描述了传输队列(Send Job Queue)功能的状态管理实现,包括 Redux 状态设计、API 集成和组件使用方法。

📁 文件结构

src/
├── domain/output/
│   └── sendJob.ts                 # 领域模型:发送任务数据类型
├── API/output/
│   ├── sendJobActions.ts          # API 层:传输队列接口(导入领域模型)
│   └── pacsNodeActions.ts         # API 层:PACS 节点接口
├── states/output/sendJob/
│   ├── types/
│   │   └── sendJobFilter.ts      # 筛选条件类型(状态层特有)
│   └── slices/
│       └── sendJobSlice.ts       # Redux 状态切片(导入领域模型)
├── states/store.ts                # Redux Store 配置
└── pages/patient/
    └── OutputList.tsx             # 传输列表页面

架构说明

本项目采用领域驱动设计(DDD)的分层架构:

  1. 领域层(Domain):定义核心业务实体,独立于技术实现
  2. API 层:负责与后端通信,使用领域模型
  3. 状态层(State):负责状态管理,使用领域模型
  4. UI 层:负责展示和交互

🎯 核心功能

1. 数据类型定义

SendJob(发送任务)- 领域模型

定义位置src/domain/output/sendJob.ts

// 从领域层导入
import { SendJob } from '@/domain/output/sendJob';

interface SendJob {
  task_id: string;              // 任务唯一标识
  patient_name: string;         // 患者姓名
  patient_id: string;           // 患者 ID
  priority: 'High' | 'Medium' | 'Low';  // 优先级
  status: 'ARRIVED' | 'SENDING' | 'FAILED' | 'SUCCESS';  // 状态
  retry_count?: number;         // 重试次数
  destination: string;          // 目标 PACS 节点名称
}

⚠️ 重要:SendJob 是领域模型,定义在 src/domain/output/sendJob.ts。API 层和状态层都应该从领域层导入此类型,确保类型定义的单一数据源。

SendJobFilter(筛选条件)

interface SendJobFilter {
  start_time?: string;   // 开始时间 (RFC3339Nano 格式)
  end_time?: string;     // 结束时间 (RFC3339Nano 格式)
  page: number;          // 当前页码
  page_size: number;     // 每页数量
}

2. Redux 状态结构

状态由 5 个 slice 组成:

{
  sendJobEntities: {
    data: SendJob[];     // 任务列表数据
    total: number;       // 总数量
  },
  sendJobFilters: {
    start_time?: string;
    end_time?: string;
    page: number;
    page_size: number;
  },
  sendJobPagination: {
    page: number;
    pageSize: number;
  },
  sendJobSelection: {
    selectedIds: string[];  // 选中的任务 ID 列表
  },
  sendJobUI: {
    loading: boolean;       // 加载状态
    error?: string | null;  // 错误信息
  }
}

3. 异步操作(Thunks)

fetchSendQueueThunk - 获取发送队列

import { fetchSendQueueThunk } from '@/states/output/sendJob/slices/sendJobSlice';

// 在组件中使用
dispatch(fetchSendQueueThunk({
  page: 1,
  pageSize: 10,
  filters: {
    start_time: '2025-10-10T00:00:00.000+08:00',
    end_time: '2025-10-10T23:59:59.999+08:00',
  }
}));

deleteSendJobThunk - 删除任务

import { deleteSendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';

// 删除单个或多个任务
dispatch(deleteSendJobThunk(['task_id_1', 'task_id_2']));

retrySendJobThunk - 重试任务

import { retrySendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';

// 重试失败的任务
dispatch(retrySendJobThunk({
  taskId: 'task_id',
  instanceUid: 'sop_instance_uid'  // 需要图像实例 UID
}));

💡 组件使用示例

基础用法

import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/states/store';
import {
  fetchSendQueueThunk,
  deleteSendJobThunk,
  retrySendJobThunk,
  sendJobActions,
} from '@/states/output/sendJob/slices/sendJobSlice';

const SendJobList: React.FC = () => {
  const dispatch = useAppDispatch();
  
  // 获取状态
  const { data: sendJobs, total } = useAppSelector(state => state.sendJobEntities);
  const { loading, error } = useAppSelector(state => state.sendJobUI);
  const filters = useAppSelector(state => state.sendJobFilters);
  const { page, pageSize } = useAppSelector(state => state.sendJobPagination);
  const { selectedIds } = useAppSelector(state => state.sendJobSelection);
  
  // 初始加载数据
  useEffect(() => {
    dispatch(fetchSendQueueThunk({ page, pageSize, filters }));
  }, [dispatch, page, pageSize, filters]);
  
  // 刷新数据
  const handleRefresh = () => {
    dispatch(fetchSendQueueThunk({ page, pageSize, filters }));
  };
  
  // 删除选中的任务
  const handleDelete = () => {
    if (selectedIds.length > 0) {
      dispatch(deleteSendJobThunk(selectedIds));
    }
  };
  
  // 重试任务
  const handleRetry = (taskId: string, instanceUid: string) => {
    dispatch(retrySendJobThunk({ taskId, instanceUid }));
  };
  
  // 设置时间范围筛选
  const handleDateRangeChange = (startTime: string, endTime: string) => {
    dispatch(sendJobActions.setStartTime(startTime));
    dispatch(sendJobActions.setEndTime(endTime));
  };
  
  // 切换页码
  const handlePageChange = (newPage: number) => {
    dispatch(sendJobActions.setPage(newPage));
  };
  
  return (
    <div>
      {/* 渲染列表 */}
      {loading && <div>加载中...</div>}
      {error && <div>错误: {error}</div>}
      
      <ul>
        {sendJobs.map(job => (
          <li key={job.task_id}>
            {job.patient_name} - {job.status}
            {job.status === 'FAILED' && (
              <button onClick={() => handleRetry(job.task_id, 'instance_uid')}>
                重试
              </button>
            )}
          </li>
        ))}
      </ul>
      
      {/* 分页 */}
      <div>
        <button onClick={() => handlePageChange(page - 1)} disabled={page === 1}>
          上一页
        </button>
        <span>第 {page} 页,共 {Math.ceil(total / pageSize)} 页</span>
        <button 
          onClick={() => handlePageChange(page + 1)} 
          disabled={page >= Math.ceil(total / pageSize)}
        >
          下一页
        </button>
      </div>
      
      {/* 操作按钮 */}
      <button onClick={handleRefresh}>刷新</button>
      <button onClick={handleDelete} disabled={selectedIds.length === 0}>
        删除选中
      </button>
    </div>
  );
};

🔧 高级用法

1. 选择管理

import { sendJobSelectionSlice } from '@/states/output/sendJob/slices/sendJobSlice';

// 选中任务
dispatch(sendJobSelectionSlice.actions.setSelectedIds(['task_id_1', 'task_id_2']));

// 清空选择
dispatch(sendJobSelectionSlice.actions.clearSelection());

2. 分页控制

import { sendJobPaginationSlice } from '@/states/output/sendJob/slices/sendJobSlice';

// 设置页码
dispatch(sendJobPaginationSlice.actions.setPage(2));

// 设置每页数量
dispatch(sendJobPaginationSlice.actions.setPageSize(20));

3. 筛选条件

import { sendJobFiltersSlice } from '@/states/output/sendJob/slices/sendJobSlice';

// 设置完整筛选条件
dispatch(sendJobFiltersSlice.actions.setFilters({
  start_time: '2025-10-10T00:00:00.000+08:00',
  end_time: '2025-10-10T23:59:59.999+08:00',
  page: 1,
  page_size: 10,
}));

// 重置筛选条件
dispatch(sendJobFiltersSlice.actions.resetFilters());

📊 状态监听

监听任务状态变化

useEffect(() => {
  // 当任务数据变化时执行操作
  console.log('当前任务列表:', sendJobs);
  console.log('总数:', total);
}, [sendJobs, total]);

useEffect(() => {
  // 当加载状态变化时
  if (loading) {
    console.log('正在加载...');
  }
}, [loading]);

useEffect(() => {
  // 当发生错误时
  if (error) {
    console.error('发生错误:', error);
    // 可以显示错误提示
  }
}, [error]);

🔄 与其他功能集成

1. 图像发送成功后自动刷新

// 在图像发送成功的处理中
const handleImageSendSuccess = () => {
  // 刷新传输队列
  dispatch(fetchSendQueueThunk({ page, pageSize, filters }));
};

2. MQTT 消息监听

如果需要实时更新任务状态,可以监听 MQTT 消息:

useEffect(() => {
  // 订阅 MQTT 消息
  const handleMqttMessage = (message: any) => {
    if (message.topic === 'send_job_status_update') {
      // 刷新列表
      dispatch(fetchSendQueueThunk({ page, pageSize, filters }));
    }
  };
  
  // 注册监听器
  mqttService.subscribe('send_job_status_update', handleMqttMessage);
  
  return () => {
    // 清理监听器
    mqttService.unsubscribe('send_job_status_update', handleMqttMessage);
  };
}, [dispatch, page, pageSize, filters]);

⚠️ 注意事项

1. 前端分页 vs 后端分页

当前实现在前端进行分页

// 在 fetchSendQueueThunk 中
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedData = response.data.slice(startIndex, endIndex);

如果后端支持分页,应该修改为:

const response = await getSendQueue({
  start_time: filters.start_time,
  end_time: filters.end_time,
  page,           // 传递页码
  page_size: pageSize,  // 传递每页数量
});

2. 重试操作需要 instance_uid

重试任务时需要提供图像实例 UID,这个信息可能需要从:

  • 任务详情中获取
  • 与 Study/Series/Image 数据关联
  • 后端 API 返回任务时包含

3. 时间格式

时间筛选使用 RFC3339Nano 格式:

// 正确格式
'2025-10-10T00:00:00.000+08:00'
'2025-10-10T23:59:59.999+08:00'

🐛 调试技巧

1. 查看 Redux 状态

使用 Redux DevTools:

// 在浏览器中查看
window.__REDUX_DEVTOOLS_EXTENSION__

2. 控制台日志

Thunks 中已包含日志:

console.log('重试发送任务,task_id:', taskId);
console.log('重试发送任务 fulfilled,result:', result);

3. 网络请求

检查浏览器 Network 标签页,查看 API 请求和响应。

📝 待完成事项

  1. WorklistTable 组件适配

    • 当前 WorklistTable 组件需要调整以支持传输队列数据
    • 需要创建专门的 SendJobTable 组件,或修改 WorklistTable 支持多种数据类型
  2. OutputOperationPanel 组件集成

    • 添加重试按钮
    • 添加删除按钮
    • 添加时间范围筛选器
  3. 实时状态更新

    • 集成 MQTT 监听传输任务状态变化
    • 自动刷新列表
  4. 错误处理优化

    • 显示友好的错误提示
    • 添加重试机制

📚 相关文档

🎓 学习资源

  • Redux 状态管理模式
  • TypeScript 类型系统
  • React Hooks 最佳实践
  • 分页和筛选实现模式