从任务清单进入检查的流程.md 24 KB

从任务清单进入检查的流程

📋 文档概述

文档名称: 从任务清单进入检查的流程总结

创建日期: 2025-12-22

文档目的: 全面总结从 Worklist/History 任务清单双击进入检查或处理界面的完整业务流程

涵盖范围:

  • 用户交互流程
  • 核心业务逻辑
  • 数据流转关系
  • Redux 状态管理
  • 路由导航机制

📊 主流程泳道图

sequenceDiagram
    participant User as 👤 用户
    participant UI as 🖥️ UI层<br/>(Worklist/History)
    participant Domain as ⚙️ Domain层<br/>(worklistToExam)
    participant API as 🌐 后端API
    participant Redux as 📦 Redux Store
    participant Router as 🔀 路由系统

    User->>UI: 双击任务行
    UI->>Domain: worklistToExam(task)
    
    Note over Domain: 1. 准备数据阶段
    Domain->>API: fetchTaskDetails(StudyID)
    API-->>Domain: 返回完整Study数据<br/>(series + images)
    
    Note over Domain: 2. 数据转换阶段
    Domain->>Domain: 转换为Task格式<br/>(包含所有Views)
    
    Note over Domain: 3. 曝光状态判断
    Domain->>Domain: 检查所有Views的<br/>expose_status
    
    alt 所有体位已曝光
        Note over Domain: 分支A: 进入处理界面
        Domain->>Redux: clearWorks()
        Domain->>Redux: addWork(updatedTask)
        Domain->>Domain: transformWorksToBodyPositions()
        Domain->>Redux: setBodyPositions(bodyPositions)
        Domain->>Redux: setBusinessFlow('process')
        Redux-->>Router: 触发路由变更
        Router-->>User: 导航到处理界面
    else 存在未曝光体位
        Note over Domain: 分支B: 进入检查界面
        Domain->>Redux: clearWorks()
        Domain->>Redux: addWork(updatedTask)
        Domain->>Redux: setBusinessFlow('exam')
        Redux-->>Router: 触发路由变更
        Router-->>User: 导航到检查界面
    end

🔀 核心决策流程图

flowchart TD
    Start([用户双击任务]) --> Fetch[调用 fetchTaskDetails API]
    Fetch --> Transform[转换数据格式<br/>Series + Images → Views]
    Transform --> Check{检查曝光状态}
    
    Check -->|Views.length > 0 &&<br/>所有 expose_status === 'Exposed'| AllExposed[所有体位已曝光]
    Check -->|存在任何<br/>expose_status !== 'Exposed'| HasUnexposed[存在未曝光体位]
    
    AllExposed --> ClearA[清空缓存 clearWorks]
    ClearA --> AddA[保存任务 addWork]
    AddA --> TransformBP[转换为体位列表<br/>transformWorksToBodyPositions]
    TransformBP --> SetBP[设置体位数据<br/>setBodyPositions]
    SetBP --> ToProcess[设置流程为 'process']
    ToProcess --> ProcessPage([进入处理界面])
    
    HasUnexposed --> ClearB[清空缓存 clearWorks]
    ClearB --> AddB[保存任务 addWork]
    AddB --> ToExam[设置流程为 'exam']
    ToExam --> ExamPage([进入检查界面])
    
    style AllExposed fill:#90EE90
    style HasUnexposed fill:#FFB6C1
    style ProcessPage fill:#4169E1,color:#fff
    style ExamPage fill:#FF8C00,color:#fff

🏊 完整业务泳道图(包含多个入口)

flowchart TB
    subgraph UserActions[用户操作层]
        W[Worklist双击]
        H[History双击]
    end
    
    subgraph DomainLogic[业务逻辑层]
        WTE[worklistToExam函数]
        PWE[prepareWorksForExam<br/>准备详细数据]
        
        subgraph DataProcess[数据处理]
            FetchAPI[调用 fetchTaskDetails API]
            MapData[映射数据:<br/>Series → Views<br/>Images → dview]
        end
        
        subgraph Decision[决策逻辑]
            CheckStatus{检查曝光状态<br/>allExposed?}
        end
    end
    
    subgraph StateManagement[状态管理层 Redux]
        ClearCache[clearWorks<br/>清空缓存]
        AddCache[addWork<br/>保存任务]
        SetBP[setBodyPositions<br/>设置体位列表]
        SetFlow[setBusinessFlow<br/>设置业务流程]
    end
    
    subgraph Routing[路由层]
        ToExamRoute[导航到 /exam]
        ToProcessRoute[导航到 /process]
    end
    
    subgraph Pages[页面层]
        ExamPage[📋 检查界面<br/>Exam Page]
        ProcessPage[🖼️ 处理界面<br/>Process Page]
    end
    
    W --> WTE
    H --> WTE
    WTE --> PWE
    PWE --> FetchAPI
    FetchAPI --> MapData
    MapData --> CheckStatus
    
    CheckStatus -->|是<br/>所有体位已曝光| PathA[处理路径]
    CheckStatus -->|否<br/>存在未曝光体位| PathB[检查路径]
    
    PathA --> ClearCache
    ClearCache --> AddCache
    AddCache --> SetBP
    SetBP --> SetFlowProcess[setBusinessFlow<br/>'process']
    SetFlowProcess --> ToProcessRoute
    ToProcessRoute --> ProcessPage
    
    PathB --> ClearCache2[clearWorks]
    ClearCache2 --> AddCache2[addWork]
    AddCache2 --> SetFlowExam[setBusinessFlow<br/>'exam']
    SetFlowExam --> ToExamRoute
    ToExamRoute --> ExamPage
    
    style W fill:#E6F3FF
    style H fill:#E6F3FF
    style CheckStatus fill:#FFF4E6
    style ExamPage fill:#FF8C00,color:#fff
    style ProcessPage fill:#4169E1,color:#fff

📋 详细流程说明

阶段1: 用户触发操作

入口点

  • Worklist 页面双击任务行
  • History 页面双击任务行

触发函数

  • handleRowDoubleClick(record: Task)worklistToExam(task)

阶段2: 数据准备 (prepareWorksForExam)

输入: Task 对象(可能包含不完整的 Views 数据)

处理流程:

  1. 调用 fetchTaskDetails(StudyID) 获取完整的 Study 数据
  2. 接收后端返回的 seriesimages 数据
  3. 使用 flatMap 将 Series 和 Images 转换为 dview[] 格式
  4. 构建包含完整 Views 的 Task 对象

输出: Task 对象(包含完整的 Views 数据)

数据映射关系:

TaskDetails.series[] → flatMap → Task.Views[]
  ├─ Series.images[] → map → dview[]
  └─ 每个 Image 映射为一个 dview 对象

dview 字段映射: | 源字段 (Image/Series) | 目标字段 (dview) | 说明 | |----------------------|------------------|------| | image.view_id | view_id | 体位ID | | series.series_instance_uid | series_instance_uid | Series UID | | taskDetails.study_instance_uid | study_instance_uid | Study UID | | image.sop_instance_uid | PrimarySopUID | 图像 SOP UID | | image.expose_status | expose_status | 曝光状态(关键) | | image.image_file_path | image_file | 图像文件路径 | | image.thumbnail_file | thumbnail_file | 缩略图路径 |


阶段3: 曝光状态判断 ⚠️ 核心决策点

判断逻辑:

const allExposed = 
  updatedTask.Views.length > 0 &&
  updatedTask.Views.every((view) => view.expose_status === 'Exposed');

决策规则:

条件 判断结果 目标界面
Views.length === 0 allExposed = false Exam(检查界面)
Views.length > 0 且所有 expose_status === 'Exposed' allExposed = true Process(处理界面)
Views.length > 0 但存在 expose_status !== 'Exposed' allExposed = false Exam(检查界面)

状态值说明:

  • 'Exposed' - 已曝光(已拍摄图像)
  • 'Unexposed' - 未曝光(待拍摄)

阶段4: Redux 状态更新

4A. 进入处理界面 (allExposed === true)

步骤顺序:

1. dispatch(clearWorks())                    // 清空现有工作缓存
2. dispatch(addWork(updatedTask))            // 保存新任务到缓存
3. bodyPositions = await transformWorksToBodyPositions([updatedTask])  // 转换为体位列表
4. dispatch(setBodyPositions(bodyPositions)) // 设置体位列表数据
5. dispatch(setBusinessFlow('process'))      // 切换业务流程到 process

Redux 状态变化: | Slice | Action | 状态变化 | |-------|--------|---------| | examWorksCache | clearWorks | works: [] | | examWorksCache | addWork | works: [updatedTask] | | bodyPositionList | setBodyPositions | positions: ExtendedBodyPosition[] | | businessFlow | setBusinessFlow | currentKey: 'process' |

关键依赖: 必须先设置 bodyPositions,否则处理界面无法正常显示


4B. 进入检查界面 (allExposed === false)

步骤顺序:

1. dispatch(clearWorks())             // 清空现有工作缓存
2. dispatch(addWork(updatedTask))     // 保存新任务到缓存
3. dispatch(setBusinessFlow('exam'))  // 切换业务流程到 exam

Redux 状态变化: | Slice | Action | 状态变化 | |-------|--------|---------| | examWorksCache | clearWorks | works: [] | | examWorksCache | addWork | works: [updatedTask] | | businessFlow | setBusinessFlow | currentKey: 'exam' |

说明: 检查界面不需要 bodyPositions,因为会在界面内动态生成和管理


阶段5: 路由导航

触发机制: setBusinessFlow 的 Redux 状态变化会触发路由监听

导航目标:

  • businessFlow.currentKey === 'exam' → 导航到 /exam 路由
  • businessFlow.currentKey === 'process' → 导航到 /process 路由

📊 数据流转关系表

阶段 输入 处理函数/逻辑 输出
1. API 调用 StudyID: string fetchTaskDetails(StudyID) TaskDetails (包含 series, images)
2. 数据转换 TaskDetails Series.flatMap(s => s.images.map(...)) Task (包含 Views)
3. 状态判断 Task.Views[] Views.every(v => v.expose_status === 'Exposed') boolean (allExposed)
4A. 体位转换 Task[] transformWorksToBodyPositions(tasks) ExtendedBodyPosition[]
4B. Redux 更新 Task, allExposed 条件分支 dispatch Redux State 更新
5. 路由导航 businessFlow.currentKey 路由监听 页面切换

🎯 四种业务场景

场景矩阵

入口 曝光状态 判断条件 目标界面 测试文件
Worklist 未曝光 allExposed = false Exam worklist-enter-exam-unexposed.cy.ts
Worklist 已曝光 allExposed = true Process worklist-enter-process-exposed.cy.ts
History 未曝光 allExposed = false Exam history-enter-exam-unexposed.cy.ts
History 已曝光 allExposed = true Process history-enter-process-exposed.cy.ts

场景1: Worklist → Exam(未曝光)

sequenceDiagram
    participant User
    participant Worklist
    participant worklistToExam
    participant API
    participant Redux
    participant ExamPage

    User->>Worklist: 双击未曝光任务
    Worklist->>worklistToExam: worklistToExam(task)
    worklistToExam->>API: fetchTaskDetails(StudyID)
    API-->>worklistToExam: TaskDetails (有未曝光体位)
    worklistToExam->>worklistToExam: allExposed = false
    worklistToExam->>Redux: clearWorks() + addWork()
    worklistToExam->>Redux: setBusinessFlow('exam')
    Redux-->>ExamPage: 导航到检查界面

关键验证点:

  • ✅ API 返回包含 expose_status: 'Unexposed' 的 images
  • businessFlow.currentKey === 'exam'
  • examWorksCache.works.length === 1
  • ✅ 检查界面主要元素可见

场景2: Worklist → Process(已曝光)

sequenceDiagram
    participant User
    participant Worklist
    participant worklistToExam
    participant API
    participant Redux
    participant ProcessPage

    User->>Worklist: 双击已曝光任务
    Worklist->>worklistToExam: worklistToExam(task)
    worklistToExam->>API: fetchTaskDetails(StudyID)
    API-->>worklistToExam: TaskDetails (所有体位已曝光)
    worklistToExam->>worklistToExam: allExposed = true
    worklistToExam->>Redux: clearWorks() + addWork()
    worklistToExam->>worklistToExam: transformWorksToBodyPositions()
    worklistToExam->>Redux: setBodyPositions() + setBusinessFlow('process')
    Redux-->>ProcessPage: 导航到处理界面

关键验证点:

  • ✅ API 返回所有 images 的 expose_status === 'Exposed'
  • businessFlow.currentKey === 'process'
  • bodyPositionList.positions.length > 0
  • ✅ 处理界面主要元素可见

场景3: History → Exam(未曝光)

流程: 与场景1相同,仅入口点不同(从 History 页面触发)


场景4: History → Process(已曝光)

流程: 与场景2相同,仅入口点不同(从 History 页面触发)


🗂️ 核心文件清单

Domain 业务逻辑层

文件路径 职责 关键函数
src/domain/patient/worklistToExam.ts 核心业务逻辑 worklistToExam(), prepareWorksForExam(), worklistToProcess()

UI 组件层

文件路径 职责 关键函数/组件
src/pages/patient/worklist.tsx Worklist 页面 handleRowDoubleClick()
src/pages/patient/HistoryList.tsx History 页面 handleRowDoubleClick()
src/pages/patient/components/WorklistTable.tsx 表格组件 onDoubleClick 事件处理

Redux 状态管理层

文件路径 Slice 名称 关键 Actions
src/states/exam/examWorksCacheSlice.ts examWorksCache clearWorks(), addWork()
src/states/exam/bodyPositionListSlice.ts bodyPositionList setBodyPositions(), transformWorksToBodyPositions()
src/states/BusinessFlowSlice.ts businessFlow setBusinessFlow()

API 层

文件路径 职责 关键函数
src/API/patient/workActions.ts 任务相关 API fetchTaskDetails()

类型定义

文件路径 定义类型
src/domain/work.ts Task 接口
src/domain/dview.ts dview 接口(体位视图)
src/domain/series.ts Series 接口
src/domain/xImage.ts XImage 接口

🔍 核心代码片段

1. worklistToExam 函数(完整逻辑)

// 文件: src/domain/patient/worklistToExam.ts
const worklistToExam = async (task: Task) => {
  const dispatch = store.dispatch;

  try {
    // 步骤1: 准备详细数据
    const [updatedTask] = await prepareWorksForExam(task);

    // 步骤2: 判断曝光状态
    const allExposed =
      updatedTask.Views.length > 0 &&
      updatedTask.Views.every((view) => view.expose_status === 'Exposed');

    // 步骤3: 清空并保存任务
    dispatch(clearWorks());
    dispatch(addWork(updatedTask));

    // 步骤4: 根据状态决定流程
    if (allExposed) {
      // 所有体位已曝光 → 进入处理界面
      const bodyPositions = await transformWorksToBodyPositions([updatedTask]);
      dispatch(setBodyPositions(bodyPositions));
      dispatch(setBusinessFlow('process'));
    } else {
      // 存在未曝光体位 → 进入检查界面
      dispatch(setBusinessFlow('exam'));
    }
  } catch (error) {
    console.error('Error in worklistToExam:', error);
    throw error;
  }
};

2. prepareWorksForExam 函数(数据准备)

// 文件: src/domain/patient/worklistToExam.ts
export const prepareWorksForExam = async (
  works: Task | Task[]
): Promise<Task[]> => {
  const workArray = Array.isArray(works) ? works : [works];

  const preparedWorks = await Promise.all(
    workArray.map(async (work) => {
      // 调用 API 获取详细数据
      const taskDetails = await fetchTaskDetails(work.StudyID);

      // 转换数据格式: Series → Views
      return {
        ...work,
        Views: taskDetails.series.flatMap<dview>((series: Series) =>
          series.images.map<dview>(
            (image: XImage) =>
              ({
                view_id: image.view_id,
                series_instance_uid: series.series_instance_uid,
                study_instance_uid: taskDetails.study_instance_uid,
                study_id: taskDetails.study_id,
                procedure_id: series.procedure_id,
                view_description: image.view_description,
                view_type: '',
                PrimarySopUID: image.sop_instance_uid,
                expose_status: image.expose_status,  // 关键字段
                judged_status: image.judged_status,
                image_file_path: image.image_file_path,
                image_file: image.image_file_path,
                thumbnail_file: image.thumbnail_file || '',
              }) satisfies dview
          )
        ),
      } as Task;
    })
  );

  return preparedWorks;
};

3. 曝光状态判断(关键逻辑)

// 判断条件
const allExposed =
  updatedTask.Views.length > 0 &&  // 1. 确保有体位数据
  updatedTask.Views.every((view) => view.expose_status === 'Exposed');  // 2. 所有体位已曝光

// 决策分支
if (allExposed) {
  // 路径A: 进入处理界面
  const bodyPositions = await transformWorksToBodyPositions([updatedTask]);
  dispatch(setBodyPositions(bodyPositions));
  dispatch(setBusinessFlow('process'));
} else {
  // 路径B: 进入检查界面
  dispatch(setBusinessFlow('exam'));
}

🧪 测试验证要点

Redux 状态验证

验证 examWorksCache

cy.window()
  .its('store')
  .invoke('getState')
  .its('examWorksCache')
  .its('works')
  .should('have.length', 1)
  .its(0)
  .should((task) => {
    expect(task).to.have.property('StudyID');
    expect(task.Views).to.be.an('array');
    expect(task.Views.length).to.be.greaterThan(0);
  });

验证 businessFlow(检查界面)

cy.window()
  .its('store')
  .invoke('getState')
  .its('businessFlow')
  .its('currentKey')
  .should('eq', 'exam');

验证 businessFlow(处理界面)

cy.window()
  .its('store')
  .invoke('getState')
  .its('businessFlow')
  .its('currentKey')
  .should('eq', 'process');

验证 bodyPositionList(处理界面)

cy.window()
  .its('store')
  .invoke('getState')
  .its('bodyPositionList')
  .its('positions')
  .should('have.length.greaterThan', 0)
  .each((bp) => {
    expect(bp).to.have.property('dview');
    expect(bp.dview).to.have.property('expose_status', 'Exposed');
  });

API Mock 验证

Mock 未曝光任务详情

cy.intercept('GET', '/dr/api/v1/auth/study/*', {
  statusCode: 200,
  body: {
    study_id: 'TEST001',
    study_instance_uid: '1.2.3.4.5',
    series: [
      {
        series_instance_uid: '1.2.3.4.5.1',
        images: [
          {
            sop_instance_uid: '1.2.3.4.5.1.1',
            view_id: 'VIEW001',
            expose_status: 'Unexposed',  // 关键: 未曝光
            // ... 其他字段
          }
        ]
      }
    ]
  }
}).as('getStudyDetailsUnexposed');

Mock 已曝光任务详情

cy.intercept('GET', '/dr/api/v1/auth/study/*', {
  statusCode: 200,
  body: {
    study_id: 'TEST002',
    study_instance_uid: '1.2.3.4.6',
    series: [
      {
        series_instance_uid: '1.2.3.4.6.1',
        images: [
          {
            sop_instance_uid: '1.2.3.4.6.1.1',
            view_id: 'VIEW002',
            expose_status: 'Exposed',  // 关键: 已曝光
            image_file_path: '/path/to/image.dcm',
            thumbnail_file: '/path/to/thumb.jpg',
            // ... 其他字段
          }
        ]
      }
    ]
  }
}).as('getStudyDetailsExposed');

🐛 边界情况处理

1. Views 为空数组

情况: updatedTask.Views.length === 0

判断结果: allExposed = false(因为前置条件失败)

处理: 进入检查界面(Exam)

原因: 没有体位数据,需要在检查界面配置


2. 混合曝光状态

情况: 部分体位 expose_status === 'Exposed',部分 'Unexposed'

判断结果: allExposed = falseevery() 条件不满足)

处理: 进入检查界面(Exam)

原因: 存在未完成的曝光任务,需要继续检查


3. expose_status 其他值

可能值:

  • 'Exposed' - 已曝光(已拍摄)
  • 'Unexposed' - 未曝光(待拍摄)
  • 可能存在其他状态(如 'Deleted', 'Archived' 等)

处理:

  • 只有完全等于 'Exposed' 才算已曝光
  • 其他任何值都视为未曝光,进入检查界面

4. API 调用失败

情况: fetchTaskDetails() 抛出错误

处理:

try {
  // ... 业务逻辑
} catch (error) {
  console.error('Error in worklistToExam:', error);
  throw error;  // 向上层抛出,由调用方处理
}

说明: 错误会冒泡到 UI 层,通常会显示错误提示


📝 设计模式和架构特点

1. 关注点分离

层次 职责 示例文件
UI 层 用户交互、事件触发 worklist.tsx, HistoryList.tsx
Domain 层 业务逻辑、数据转换 worklistToExam.ts
API 层 后端通信 workActions.ts
State 层 状态管理 examWorksCacheSlice.ts, businessFlowSlice.ts

2. 单一职责原则

  • prepareWorksForExam() - 仅负责数据准备和转换
  • worklistToExam() - 负责业务逻辑和流程控制
  • transformWorksToBodyPositions() - 仅负责体位列表转换

3. 异步流程处理

所有 API 调用和数据转换都使用 async/await 模式:

const [updatedTask] = await prepareWorksForExam(task);
const bodyPositions = await transformWorksToBodyPositions([updatedTask]);

4. 数据不可变性

使用 Redux Toolkit 的 Immer,确保状态更新的不可变性:

dispatch(clearWorks());  // 不直接修改 state,而是 dispatch action
dispatch(addWork(updatedTask));

🚀 性能优化考虑

1. 并行数据获取

prepareWorksForExam 使用 Promise.all 并行处理多个任务:

const preparedWorks = await Promise.all(
  workArray.map(async (work) => {
    const taskDetails = await fetchTaskDetails(work.StudyID);
    return { ...work, Views: ... };
  })
);

优势: 多个任务同时请求,减少总等待时间


2. 条件性体位转换

只有在进入处理界面时才执行体位转换:

if (allExposed) {
  const bodyPositions = await transformWorksToBodyPositions([updatedTask]);
  dispatch(setBodyPositions(bodyPositions));
  dispatch(setBusinessFlow('process'));
}

优势: 避免不必要的数据转换计算


3. 缓存清理

每次进入前清空旧缓存,避免数据冲突:

dispatch(clearWorks());  // 清理旧数据
dispatch(addWork(updatedTask));  // 添加新数据

📚 相关文档索引

实现文档

测试文档

代码文件


✅ 总结

核心要点

  1. 双击触发: 从 Worklist 或 History 双击任务行触发流程
  2. 数据准备: 通过 API 获取完整的 Study 数据,转换为 Views
  3. 状态判断: 根据所有体位的 expose_status 判断是否全部曝光
  4. 流程分支:
    • 全部已曝光 → 处理界面 (Process)
    • 存在未曝光 → 检查界面 (Exam)
  5. 状态管理: 通过 Redux 管理任务缓存、体位列表和业务流程

关键判断逻辑

const allExposed = 
  updatedTask.Views.length > 0 &&
  updatedTask.Views.every((view) => view.expose_status === 'Exposed');

测试覆盖

  • ✅ Worklist → Exam (未曝光)
  • ✅ Worklist → Process (已曝光)
  • ✅ History → Exam (未曝光)
  • ✅ History → Process (已曝光)

文档版本: v1.0
最后更新: 2025-12-22
维护者: Development Team