云
昨天 c3d88ddd346f53aef4beced518e3f6f0bb815fa4
fix(qr-export): 解决下载接口业务异常处理问题

- 添加readBusinessError函数处理后端业务异常JSON响应
- 实现nonRetryable函数标记不可重试的业务错误
- 在requestChunk中检测并抛出不可重试的业务错误
- 处理服务器忽略Range请求返回整包的情况
- 在分段下载重试逻辑中跳过不可重试的错误
- 在use-qr-export中检查任务可下载状态并显示过期提示
已修改2个文件
43 ■■■■■ 文件已修改
src/views/mes/pro/qrexport/download-qr-export.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/qrexport/use-qr-export.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/qrexport/download-qr-export.ts
@@ -23,6 +23,29 @@
  }
}
/**
 * 后端业务异常统一以 HTTP 200 + JSON 返回;下载接口的文件过期、任务状态异常等都属于此类。
 * 这里把真实原因提取出来,避免被误报成「分段下载失败(HTTP 200)」。
 */
async function readBusinessError(response: Response): Promise<null | string> {
  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.includes('json')) return null;
  try {
    const body = await response.clone().json();
    if (body && typeof body.msg === 'string' && body.msg) return body.msg;
  } catch {
    // 非标准 JSON 交给调用方按通用错误处理
  }
  return null;
}
/** 业务错误重试无意义(如文件已过期),直接终止。 */
function nonRetryable(message: string): Error {
  const error = new Error(message) as Error & { retryable?: boolean };
  error.retryable = false;
  return error;
}
async function requestChunk(
  url: string,
  token: string | undefined,
@@ -41,6 +64,14 @@
        signal,
      });
      if (response.status !== 206) {
        const businessError = await readBusinessError(response);
        if (businessError) throw nonRetryable(businessError);
        const contentType = response.headers.get('content-type') ?? '';
        if (response.status === 200
          && (contentType.includes('zip') || contentType.includes('octet-stream'))) {
          // 服务端忽略了 Range,直接回整包;继续分段只会反复下载同一份文件
          throw nonRetryable('服务器未启用分段下载,请升级后端后重试');
        }
        throw new Error(`分段下载失败(HTTP ${response.status})`);
      }
      if (response.headers.get('content-range') !== `bytes ${start}-${end}/${total}`) {
@@ -52,7 +83,8 @@
      }
      return bytes;
    } catch (error) {
      if (signal.aborted || attempt === 2) throw error;
      const retryable = (error as { retryable?: boolean }).retryable !== false;
      if (signal.aborted || !retryable || attempt === 2) throw error;
      await new Promise((resolve) => setTimeout(resolve, 750 * (attempt + 1)));
    }
  }
@@ -74,6 +106,8 @@
  const match = /^bytes 0-0\/(\d+)$/.exec(response.headers.get('content-range') ?? '');
  const total = Number(match?.[1]);
  if (response.status !== 206 || !Number.isSafeInteger(total) || total <= 0) {
    const businessError = await readBusinessError(response);
    if (businessError) throw nonRetryable(businessError);
    throw new Error('无法获取导出文件大小,请刷新任务后重试');
  }
  await response.arrayBuffer();
src/views/mes/pro/qrexport/use-qr-export.ts
@@ -208,6 +208,13 @@
      // 待下载
      stopPolling();
      state.progress = 100;
      if (!task.downloadable) {
        state.phase = 'failed';
        state.downloadRetryable = false;
        state.errorMsg = '导出文件已过期,请重新生成';
        state.message = '导出文件已过期,请重新生成';
        return;
      }
      await downloadProduct(task);
      return;
    }