ai-coding
2026-06-08 bdc06cd4388fad2bf7e7b0ee31a333a80a54285f
**feat(order): 添加虚拟订单批处理功能-单车故障修复后**

Co-Authored-By: 黄星炤 <huangxz@xiaopeng.com>
4个文件已修改
9863 ■■■■■ 已修改文件
docs/virtualOrderFeat.md 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json 9757 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/copyText.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanList/add.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/virtualOrderFeat.md
@@ -44,7 +44,7 @@
| --- | --- |
| 订单 ID 必须能在进度表中查到 | `未找到订单对应的车辆进度: {orderId}` |
| 订单所属计划/车辆必须与传入参数一致 | `订单与测试计划/车辆不匹配,无法恢复` |
| 车辆进度状态必须是 RUNNING(0) | `车辆进度非运行中,无法恢复` |
| 车辆进度状态必须是 RUNNING(0) 或 ERROR(2) | `车辆进度状态不允许恢复(仅RUNNING/ERROR可操作)` |
**幂等性**:恢复成功后,车辆进度的 `currentOrderId` 会被链式回调清空并替换为下一段订单 ID。前端再传旧 `currentOrderId` 会触发"订单与测试计划/车辆不匹配"。
@@ -55,7 +55,7 @@
调用 `manualResume` 之前,必须确认以下信息(建议从 `vehicleProgress` 接口获取):
1. 当前批次状态为 RUNNING(批次已取消则恢复无意义)
2. 目标车辆 `status` = `RUNNING`(COMPLETED / ERROR / CANCELLED 不能恢复)
2. 目标车辆 `status` = `RUNNING` 或 `ERROR`(COMPLETED / CANCELLED 不能恢复)
3. 当前 `currentOrderId` 真实存在且确实卡死(建议先与现场人员确认)
---
@@ -66,15 +66,16 @@
批量测试详情页 → 车辆进度列表 → 每行操作列。
### 显示条件
仅当 **`status === RUNNING`** 时显示「故障恢复」按钮。
当 **`status === RUNNING` 或 `status === ERROR`** 时显示「故障恢复」按钮。
### 二次确认弹窗
执行前必须强提示用户:
> ⚠️ 故障恢复将跳过订单 `{currentOrderId}`,并立即派发该车辆的下一段订单。
> - 此操作**仅适用于车辆故障已修复、旧订单确认无法继续的场景**
> - 此操作**适用于车辆故障已修复、旧订单确认无法继续的场景**(RUNNING 卡死或 ERROR 需重跑)
> - 旧订单将被标记为「已取消」,**不会**通知车端
> - 操作前请确认车端已不再执行旧订单,否则可能与新订单冲突
> - ERROR 状态的车辆执行后将重新进入 RUNNING 并派发下一段订单
>
> 是否继续?
@@ -149,6 +150,38 @@
---
## 五-B、后端改动要求(本次设计变更)
> **变更**:故障恢复接口同时支持 `RUNNING` 和 `ERROR` 状态的车辆。
后端 `manualResume` 方法需调整:
1. 状态校验放宽:`status == RUNNING || status == ERROR` 均允许通过
2. ERROR 状态的车辆恢复时,需先将进度状态重置为 `RUNNING`(同 `batchRetry` 逻辑),再执行链式回调推进
3. 错误信息清空:`errorMsg` 置 null
```java
// 原校验
if (progress.getStatus() == null || progress.getStatus() != VEHICLE_STATUS_RUNNING) {
    throw new RuntimeException("车辆进度非运行中,无法恢复");
}
// 改为
if (progress.getStatus() == null ||
    (progress.getStatus() != VEHICLE_STATUS_RUNNING && progress.getStatus() != VEHICLE_STATUS_ERROR)) {
    throw new RuntimeException("车辆进度状态不允许恢复(仅RUNNING/ERROR可操作)");
}
// ERROR 状态需先重置
if (progress.getStatus() == VEHICLE_STATUS_ERROR) {
    progress.setStatus(VEHICLE_STATUS_RUNNING);
    progress.setErrorMsg(null);
    progress.setUpdateTime(LocalDateTime.now());
    vehicleProgressMapper.updateVehicleProgress(progress);
}
```
---
## 六、注意事项与对接 FAQ
**Q1:故障恢复后,统计数字会怎么变化?**
@@ -166,7 +199,7 @@
**Q5:恢复失败后该怎么办?**
A:根据 `msg` 提示分情况:
- "未找到订单"/"不匹配" → 数据已变化,先刷新进度列表
- "非运行中" → 该车辆已 ERROR/CANCELLED,使用 `batchRetry` 而非本接口
- "车辆进度状态不允许恢复" → 该车辆已 COMPLETED/CANCELLED,无法恢复
- 网络/系统异常 → 建议提示用户稍后重试
**Q6:是否需要权限控制?**
package-lock.json
Diff too large
src/service/api/copyText.ts
@@ -233,3 +233,8 @@
export function batchCancelTestPlan(testPlanId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/batchCancel', null, { params: { testPlanId } })
}
/** 批量测试 - 故障车辆手动恢复 */
export function manualResumeVehicle(params: { testPlanId: number | string; vehicleId: number | string; currentOrderId: number | string }) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/manualResume', null, { params })
}
src/views/testDemo/testPlanList/add.vue
@@ -20,6 +20,7 @@
  fetchTestOrderPageList,
  fetchTestPlanDetail,
  fetchVehicleProgress,
  manualResumeVehicle,
  reassignTestOrderById,
} from '@/service/api/copyText'
import { getAvailableVehicleList } from '@/service/api/transaction'
@@ -1030,7 +1031,7 @@
  })
}
const vehicleProgressTableScrollX = 960
const vehicleProgressTableScrollX = 1060
const vehicleProgressColumns = computed<DataTableColumns<any>>(() => [
  {
    title: '序号',
@@ -1081,8 +1082,63 @@
    ellipsis: true,
    render: row => row.errorMsg || '-',
  },
  {
    title: '操作',
    key: 'actions',
    width: 100,
    align: 'left',
    fixed: 'right' as const,
    render: (row: any) => {
      if ((row.status === 'RUNNING' || row.status === 'ERROR') && row.currentOrderId && isBatchRunning.value) {
        return (
          <n-button text type="warning" size="small" onClick={() => handleManualResume(row)}>
            故障恢复
          </n-button>
        )
      }
      return '-'
    },
  },
])
const manualResumeLoading = ref(false)
function handleManualResume(row: any) {
  if (manualResumeLoading.value) return
  if (!row.currentOrderId) {
    message.warning('当前订单ID为空,请刷新后重试')
    return
  }
  dialog.warning({
    title: '故障恢复确认',
    content: `⚠️ 故障恢复将跳过订单 ${row.currentOrderId},并立即派发该车辆的下一段订单。\n\n• 此操作适用于车辆故障已修复、旧订单确认无法继续的场景\n• 旧订单将被标记为「已取消」,不会通知车端\n• 操作前请确认车端已不再执行旧订单,否则可能与新订单冲突\n• ERROR 状态的车辆执行后将重新进入 RUNNING 并派发下一段订单\n\n是否继续?`,
    positiveText: '确定',
    negativeText: '取消',
    maskClosable: false,
    autoFocus: false,
    onPositiveClick: async () => {
      manualResumeLoading.value = true
      try {
        const res = await manualResumeVehicle({
          testPlanId: planId.value,
          vehicleId: row.vehicleId,
          currentOrderId: row.currentOrderId,
        })
        if (res.code === 200) {
          message.success('已触发故障恢复')
          await refreshBatchData()
        }
        else {
          message.error(res?.message || res?.msg || '故障恢复失败')
        }
      }
      finally {
        manualResumeLoading.value = false
      }
    },
  })
}
// 监听批次状态,自动停止轮询
watch(() => batchProgress.value?.status, (status) => {
  if (status && status !== 'RUNNING') {