Parcourir la source

feat: 实现传输队列重试发送任务功能

- 修改API层retrySendTask函数,参数从instance_uid改为task_id
- 简化状态管理层retrySendJobThunk参数,去除instanceUid
- 在OutputActionPanel组件中实现重试按钮点击处理逻辑
- 支持批量重试多个选中任务,使用Promise.all并行执行
- 添加完善的用户反馈(警告、成功、错误提示)
- 重试成功后自动更新任务状态为SENDING,retry_count自动增加
- 新增完整的实现文档,包含交互流程图、数据结构和测试要点

改动文件:
- src/API/output/sendJobActions.ts
- src/states/output/sendJob/slices/sendJobSlice.ts
- src/pages/patient/components/OutputActionPanel.tsx
- docs/实现/重试发送任务功能.md (新增)
sw il y a 3 jours
Parent
commit
a5a03db918

+ 429 - 0
docs/实现/重试发送任务功能.md

@@ -0,0 +1,429 @@
+# 重试发送任务功能实现文档
+
+## 功能概述
+
+实现了传输队列中的【重试发送任务】功能,允许用户选择失败的发送任务并重新提交到后端执行。
+
+## 一、涉及的参与者
+
+### 1. 页面层(Page Layer)
+- **OutputlistPage** (`src/pages/patient/OutputList.tsx`)
+  - 主容器页面,负责展示传输队列
+  - 管理表格、分页和操作面板
+  - 维护任务选中状态
+
+### 2. 组件层(Component Layer)
+- **OutputOperationPanel** (`src/pages/patient/components/OutputOperationPanel.tsx`)
+  - 操作面板容器组件
+  
+- **OutputActionPanel** (`src/pages/patient/components/OutputActionPanel.tsx`)
+  - 包含重试和删除操作按钮
+  - 实现重试按钮的点击处理逻辑
+
+### 3. 状态管理层(State Management Layer)
+- **sendJobSlice** (`src/states/output/sendJob/slices/sendJobSlice.ts`)
+  - `retrySendJobThunk`: 重试任务的异步 Thunk
+  - `createRetryHandlers`: 处理重试结果的 handlers
+  
+- **Redux Store**
+  - `sendJobSelection.selectedIds`: 维护选中的任务 IDs
+  - `sendJobEntities.data`: 存储任务数据
+
+### 4. API 层(API Layer)
+- **sendJobActions** (`src/API/output/sendJobActions.ts`)
+  - `retrySendTask(taskId)`: 调用后端重试接口
+  - `RetrySendTaskRequest`: 请求参数类型
+  - `RetrySendTaskResponse`: 响应数据类型
+
+### 5. 领域模型层(Domain Layer)
+- **SendJob** (`src/domain/output/sendJob.ts`)
+  - 任务实体模型
+  - 包含 `task_id`、`status`、`retry_count` 等字段
+
+## 二、实现修改清单
+
+### 2.1 API 层修改
+
+**文件:** `src/API/output/sendJobActions.ts`
+
+#### 修改内容:
+
+1. **修改请求参数接口**
+```typescript
+// 修改前
+export interface RetrySendTaskRequest {
+  instance_uid: string;
+}
+
+// 修改后
+export interface RetrySendTaskRequest {
+  task_id: string;
+}
+```
+
+2. **修改 retrySendTask 函数**
+```typescript
+// 修改前
+export const retrySendTask = async (
+  instanceUid: string
+): Promise<RetrySendTaskResponse> => {
+  const response = await axiosInstance.post<RetrySendTaskResponse>(
+    '/auth/scp/store_reply',
+    { instance_uid: instanceUid }
+  );
+  // ...
+}
+
+// 修改后
+export const retrySendTask = async (
+  taskId: string
+): Promise<RetrySendTaskResponse> => {
+  const response = await axiosInstance.post<RetrySendTaskResponse>(
+    '/auth/scp/store_reply',
+    { task_id: taskId }
+  );
+  // ...
+}
+```
+
+### 2.2 状态管理层修改
+
+**文件:** `src/states/output/sendJob/slices/sendJobSlice.ts`
+
+#### 修改内容:
+
+1. **简化 retrySendJobThunk 参数**
+```typescript
+// 修改前
+export const retrySendJobThunk = createAsyncThunk(
+  'sendJob/retry',
+  async ({ taskId, instanceUid }: { taskId: string; instanceUid: string }) => {
+    const result = await retrySendTask(instanceUid);
+    return { taskId, result };
+  }
+);
+
+// 修改后
+export const retrySendJobThunk = createAsyncThunk(
+  'sendJob/retry',
+  async (taskId: string) => {
+    const result = await retrySendTask(taskId);
+    return { taskId, result };
+  }
+);
+```
+
+### 2.3 组件层修改
+
+**文件:** `src/pages/patient/components/OutputActionPanel.tsx`
+
+#### 修改内容:
+
+1. **添加必要的导入**
+```typescript
+import { message } from 'antd';
+import { useAppDispatch, useAppSelector } from '@/states/store';
+import { retrySendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
+```
+
+2. **添加重试处理逻辑**
+```typescript
+const OutputActionPanel: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const selectedIds = useAppSelector((state) => state.sendJobSelection.selectedIds);
+
+  // 处理重试按钮点击
+  const handleRetry = async () => {
+    // 校验是否有选中的任务
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要重试的任务');
+      return;
+    }
+
+    try {
+      // 遍历选中的任务,逐个调用重试 API
+      const retryPromises = selectedIds.map((taskId) =>
+        dispatch(retrySendJobThunk(taskId)).unwrap()
+      );
+
+      await Promise.all(retryPromises);
+      message.success(`成功提交 ${selectedIds.length} 个重试任务`);
+    } catch (error) {
+      console.error('重试任务失败:', error);
+      message.error('重试任务失败,请稍后再试');
+    }
+  };
+
+  return (
+    // ...
+  );
+};
+```
+
+3. **添加按钮点击事件**
+```typescript
+<Button
+  type="default"
+  icon={/* ... */}
+  aria-label="重试"
+  style={{ width: '1.5rem', height: '1.5rem' }}
+  onClick={handleRetry}  // 添加此行
+/>
+```
+
+## 三、组件交互流程
+
+### 3.1 用户操作流程
+
+```mermaid
+sequenceDiagram
+    participant User as 用户
+    participant Button as 重试按钮
+    participant Handler as handleRetry
+    participant Redux as Redux Store
+    participant Thunk as retrySendJobThunk
+    participant API as retrySendTask API
+    participant Backend as 后端服务
+
+    User->>Button: 1. 点击重试按钮
+    Button->>Handler: 2. 触发 onClick
+    Handler->>Redux: 3. 获取 selectedIds
+    
+    alt 没有选中任务
+        Handler->>User: 4a. 显示警告消息
+    else 有选中任务
+        loop 每个选中的任务
+            Handler->>Thunk: 4b. dispatch(retrySendJobThunk(taskId))
+            Thunk->>API: 5. 调用 retrySendTask(taskId)
+            API->>Backend: 6. POST /auth/scp/store_reply
+            Backend-->>API: 7. 返回结果
+            API-->>Thunk: 8. 返回 response
+            Thunk->>Redux: 9. 更新任务状态
+        end
+        Handler->>User: 10. 显示成功消息
+    end
+```
+
+### 3.2 数据流向
+
+```
+1. 用户选择任务
+   └─> handleRowClick (OutputList)
+       └─> dispatch(setSelectedIds([taskId]))
+           └─> Redux: sendJobSelection.selectedIds = [taskId]
+
+2. 用户点击重试
+   └─> handleRetry (OutputActionPanel)
+       └─> 读取 selectedIds
+           └─> 遍历并 dispatch(retrySendJobThunk(taskId))
+               └─> retrySendTask(taskId) API 调用
+                   └─> POST /auth/scp/store_reply { task_id }
+                       └─> 后端处理并返回结果
+                           └─> Redux 更新任务状态
+                               └─> 表格自动刷新
+```
+
+## 四、相关数据结构
+
+### 4.1 SendJob 实体
+```typescript
+interface SendJob {
+  task_id: string;           // 任务唯一标识
+  patient_name: string;      // 患者姓名
+  patient_id: string;        // 患者 ID
+  priority: SendJobPriority; // 优先级: High/Medium/Low
+  status: SendJobStatus;     // 状态: ARRIVED/SENDING/FAILED/SUCCESS
+  retry_count?: number;      // 重试次数
+  destination: string;       // 目标 PACS 节点
+}
+```
+
+### 4.2 API 请求/响应
+```typescript
+// 请求参数
+interface RetrySendTaskRequest {
+  task_id: string;
+}
+
+// 响应数据
+interface RetrySendTaskResponse {
+  code: string;              // 响应码:0x000000 表示成功
+  description: string;       // 描述信息
+  solution: string;          // 解决方案
+  data: StoreReplyData;
+}
+
+interface StoreReplyData {
+  '@type': string;
+  ok: boolean;              // 是否成功
+  output: string;           // 输出信息
+}
+```
+
+### 4.3 Redux State
+```typescript
+// 选中状态
+interface SelectionState {
+  selectedIds: string[];    // 选中的 task_id 数组
+}
+
+// 任务列表状态
+interface EntitiesState<SendJob> {
+  data: SendJob[];          // 任务数组
+  total: number;            // 总数
+}
+```
+
+## 五、功能特性
+
+### 5.1 用户体验优化
+
+1. **校验提示**
+   - 未选中任务时显示警告:"请先选择要重试的任务"
+   
+2. **成功反馈**
+   - 显示具体重试的任务数量:"成功提交 N 个重试任务"
+   
+3. **错误处理**
+   - API 调用失败时显示友好错误消息
+   - Console 输出详细错误信息便于调试
+
+4. **批量操作**
+   - 支持同时重试多个任务
+   - 使用 `Promise.all` 并行执行,提高效率
+
+### 5.2 状态自动更新
+
+重试成功后,Redux Store 会自动更新:
+- `status` 更新为 `'SENDING'`
+- `retry_count` 自动增加 1
+- 表格自动刷新显示最新状态
+
+### 5.3 错误容错
+
+- API 层捕获并抛出错误
+- Thunk 层处理 rejected 状态
+- 组件层显示用户友好的错误提示
+
+## 六、测试要点
+
+### 6.1 功能测试
+
+1. **正常流程**
+   - [ ] 选中单个任务,点击重试,验证成功
+   - [ ] 选中多个任务,点击重试,验证批量成功
+   - [ ] 验证重试后任务状态更新为 SENDING
+   - [ ] 验证 retry_count 正确增加
+
+2. **边界情况**
+   - [ ] 未选中任务时点击重试,显示警告
+   - [ ] 选中后取消选中,再点击重试,显示警告
+
+3. **错误处理**
+   - [ ] 模拟网络错误,验证错误提示
+   - [ ] 模拟后端返回错误码,验证错误处理
+
+### 6.2 集成测试
+
+1. **与表格的集成**
+   - [ ] 验证选中状态正确同步
+   - [ ] 验证表格数据更新后正确显示
+
+2. **与分页的集成**
+   - [ ] 跨页选中任务,验证重试功能
+
+## 七、API 接口说明
+
+### 7.1 重试接口
+
+**端点:** `POST /auth/scp/store_reply`
+
+**请求体:**
+```json
+{
+  "task_id": "0199cd46-82f0-76c5-b1d3-9399668a1a05"
+}
+```
+
+**响应示例(成功):**
+```json
+{
+  "code": "0x000000",
+  "description": "Success",
+  "solution": "",
+  "data": {
+    "@type": "StoreReply",
+    "ok": true,
+    "output": "Successfully retried task"
+  }
+}
+```
+
+**响应示例(失败):**
+```json
+{
+  "code": "0x000001",
+  "description": "Task not found",
+  "solution": "Please check the task ID",
+  "data": {
+    "@type": "StoreReply",
+    "ok": false,
+    "output": "Task does not exist"
+  }
+}
+```
+
+## 八、注意事项
+
+1. **参数变更**
+   - 原设计使用 `instance_uid`,实际实现改为使用 `task_id`
+   - 确保后端接口支持 `task_id` 参数
+
+2. **状态更新**
+   - 重试成功后状态设为 `SENDING`,而非 `SUCCESS`
+   - 实际成功与否需要后续状态轮询确认
+
+3. **并发控制**
+   - 当前实现使用 `Promise.all` 并行重试
+   - 如需控制并发数,可使用 `Promise.allSettled` 或限流
+
+4. **权限控制**
+   - 当前未做权限检查
+   - 如需要,应在按钮层或 API 层添加权限校验
+
+## 九、未来改进方向
+
+1. **加载状态**
+   - 添加按钮 loading 状态,防止重复点击
+   - 显示重试进度(如 "重试中... 2/5")
+
+2. **单独重试**
+   - 在表格行中添加单独的重试按钮
+   - 支持直接重试单个任务,无需选中
+
+3. **重试策略**
+   - 支持配置重试次数限制
+   - 支持指数退避重试
+
+4. **结果反馈**
+   - 显示详细的重试结果(成功 N 个,失败 M 个)
+   - 失败时显示具体原因
+
+## 十、总结
+
+本次实现完成了传输队列的重试发送任务功能,主要包括:
+
+1. ✅ 修改 API 层参数,从 `instance_uid` 改为 `task_id`
+2. ✅ 简化状态管理层 Thunk 参数
+3. ✅ 实现组件层重试逻辑和用户交互
+4. ✅ 支持批量重试多个任务
+5. ✅ 实现完善的错误处理和用户反馈
+6. ✅ 自动更新任务状态和重试计数
+
+该功能已完全集成到现有的传输队列管理系统中,遵循了项目的架构设计和编码规范。
+
+---
+
+**实现日期:** 2025-10-11  
+**实现人员:** Cline  
+**文档版本:** 1.0

+ 6 - 6
src/API/output/sendJobActions.ts

@@ -88,8 +88,8 @@ export interface DeleteSendTaskRequest {
  * 重试发送任务请求参数
  */
 export interface RetrySendTaskRequest {
-  /** 图像实例 UID */
-  instance_uid: string;
+  /** 任务 ID */
+  task_id: string;
 }
 
 /**
@@ -303,26 +303,26 @@ export const deleteSendTask = async (
 
 /**
  * 重试发送任务
- * @param instanceUid 图像实例 UID
+ * @param taskId 任务 ID
  * @returns 重试结果,包含成功状态和输出信息
  * @throws 当重试失败时抛出错误
  * 
  * @example
  * ```typescript
  * const result = await retrySendTask(
- *   '1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671'
+ *   '0199cd46-82f0-76c5-b1d3-9399668a1a05'
  * );
  * console.log('重试成功:', result.data.ok);
  * ```
  */
 export const retrySendTask = async (
-  instanceUid: string
+  taskId: string
 ): Promise<RetrySendTaskResponse> => {
   try {
     const response = await axiosInstance.post<RetrySendTaskResponse>(
       '/auth/scp/store_reply',
       {
-        instance_uid: instanceUid,
+        task_id: taskId,
       }
     );
 

+ 33 - 3
src/pages/patient/components/OutputActionPanel.tsx

@@ -1,8 +1,36 @@
-import { Tooltip, Button, Space } from 'antd';
+import { Tooltip, Button, Space, message } from 'antd';
 import { FormattedMessage } from 'react-intl';
 import Icon from '@/components/Icon';
+import { useAppDispatch, useAppSelector } from '@/states/store';
+import { retrySendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
 
-const OutputActionPanel: React.FC = () => (
+const OutputActionPanel: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const selectedIds = useAppSelector((state) => state.sendJobSelection.selectedIds);
+
+  // 处理重试按钮点击
+  const handleRetry = async () => {
+    // 校验是否有选中的任务
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要重试的任务');
+      return;
+    }
+
+    try {
+      // 遍历选中的任务,逐个调用重试 API
+      const retryPromises = selectedIds.map((taskId) =>
+        dispatch(retrySendJobThunk(taskId)).unwrap()
+      );
+
+      await Promise.all(retryPromises);
+      message.success(`成功提交 ${selectedIds.length} 个重试任务`);
+    } catch (error) {
+      console.error('重试任务失败:', error);
+      message.error('重试任务失败,请稍后再试');
+    }
+  };
+
+  return (
   <Space>
     <Tooltip
       title={
@@ -26,6 +54,7 @@ const OutputActionPanel: React.FC = () => (
         }
         aria-label="重试"
         style={{ width: '1.5rem', height: '1.5rem' }}
+        onClick={handleRetry}
       />
     </Tooltip>
     <Tooltip
@@ -53,6 +82,7 @@ const OutputActionPanel: React.FC = () => (
       />
     </Tooltip>
   </Space>
-);
+  );
+};
 
 export default OutputActionPanel;

+ 3 - 3
src/states/output/sendJob/slices/sendJobSlice.ts

@@ -56,9 +56,9 @@ export const deleteSendJobThunk = createDeleteThunk(
  */
 export const retrySendJobThunk = createAsyncThunk(
   'sendJob/retry',
-  async ({ taskId, instanceUid }: { taskId: string; instanceUid: string }) => {
-    console.log(`重试发送任务,task_id: ${taskId}, instance_uid: ${instanceUid}`);
-    const result = await retrySendTask(instanceUid);
+  async (taskId: string) => {
+    console.log(`重试发送任务,task_id: ${taskId}`);
+    const result = await retrySendTask(taskId);
     return { taskId, result };
   }
 );