zhangjq
2026-02-09 4a63a68bf480dd9d58481c100e98fbc84d3368c0
根据最新批量导入文档修改本地已测试成功
2个文件已修改
438 ■■■■■ 已修改文件
src/views/contract/BatchImport.vue 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/form/stepxieyi/Step3.vue 145 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/BatchImport.vue
@@ -381,10 +381,10 @@
    }
  };
  
  // 终极优化:handleImport 方法(强制确保有效File对象,去掉有问题的签名)
  // 完全重写:handleImport 方法,只使用 defHttp 发送请求
  const handleImport = async () => {
    try {
      // 1. 前置表单验证(严格校验)
      // 1. 前置表单验证
      if (!formState.contractId) {
        createMessage.error('请选择合同');
        return;
@@ -409,71 +409,70 @@
      importErrors.value = [];
      showResult.value = false;
      
      // 3. 强制获取 AntD Upload 原始 File 对象(核心!确保是有效File实例)
      // 3. 获取文件对象
      const file = fileList.value[0];
      const fileObj = file.originFileObj; // 使用 originFileObj,这是 AntD Upload 组件的原始文件对象
      const fileObj = file.originFileObj;
      
      // 双重校验:确保 fileObj 是 File 实例
      if (!(fileObj instanceof File)) {
        createMessage.error('获取文件失败,请重新选择文件');
        importLoading.value = false;
        return;
      }
      console.log('有效File对象:', fileObj);
      console.log('===== 批量导入请求信息 =====');
      console.log('文件对象:', fileObj);
      console.log('文件名:', fileObj.name);
      console.log('文件大小:', fileObj.size);
      
      // 4. 构建业务参数
      const importParam = {
        contractId: formState.contractId,
        ranking: "3", // 签约排名写死为3
        status: "7", // 状态写死为7
        remark: "批量订单", // 备注信息写死为批量订单
        month: "12", // 合作月份写死为12个月
        price: "0", // 价格写死为0
        acceptindicator: "80" // 验收指标写死为80
        ranking: "3",
        status: "1",
        remark: "批量订单",
        month: "3",
        price: "3000",
        acceptindicator: "80",
        // 尝试在importParam中添加文案编辑参数
        changer: formState.editor,
        editorId: formState.editor,
        userId: formState.editor
      };
      console.log('业务参数:', importParam);
      
      // 5. 构建 FormData - 确保正确创建 FormData 对象
      // 5. 构建 FormData
      const formData = new FormData();
      // 验证 fileObj 是真正的 File 对象
      if (!(fileObj instanceof File) && !(fileObj instanceof Blob)) {
        createMessage.error('文件对象无效,请重新选择文件');
        importLoading.value = false;
        return;
      }
      // 添加文件到 FormData(关键:必须使用 File 对象,不能是其他格式)
      formData.append('file', fileObj, fileObj.name);
      // 添加业务参数(必须序列化为 JSON 字符串)
      formData.append('importParam', JSON.stringify(importParam));
      
      // 调试信息:验证 FormData 内容
      console.log('FormData 内容验证:');
      console.log('- file 字段:', formData.get('file'));
      console.log('- importParam 字段:', formData.get('importParam'));
      console.log('- file 类型:', fileObj instanceof File ? 'File' : fileObj instanceof Blob ? 'Blob' : typeof fileObj);
      // 添加changer参数 - API文档要求的必填参数
      formData.append('changer', formState.editor);
      console.log('添加changer参数:', formState.editor);
      
      // 6. 获取登录 Token
      // 尝试添加其他可能的参数名
      formData.append('editorId', formState.editor);
      formData.append('userId', formState.editor);
      console.log('FormData 内容:');
      for (const [key, value] of formData.entries()) {
        console.log(`- ${key}: ${typeof value === 'string' ? value : value.name}`);
      }
      // 6. 使用 XMLHttpRequest 手动构建请求,绕过拦截器问题
      console.log('使用 XMLHttpRequest 发送请求...');
      // 构建请求 URL
      const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL || 'http://192.168.31.222:8080/jeecg-boot';
      const url = `${domainUrl}/api/excel/importContract`;
      console.log('请求 URL:', url);
      // 获取登录 Token
      const token = getToken();
      if (!token) {
        createMessage.error('未登录,请先登录系统');
        importLoading.value = false;
        return;
      }
      // 7. 发送请求 - 使用 XMLHttpRequest 手动构建 multipart/form-data 请求
      // 这样可以完全控制请求体和请求头,确保 Content-Type 被正确设置
      const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL || 'http://192.168.31.222:8080/jeecg-boot';
      const url = `${domainUrl}/api/excel/importContract`;
      console.log('===== [v9] 手动构建 multipart/form-data 请求(完全控制) =====');
      console.log('请求 URL:', url);
      console.log('文件对象:', fileObj);
      console.log('业务参数:', importParam);
      
      // 手动构建 multipart/form-data 请求
      const responseData = await new Promise((resolve, reject) => {
@@ -492,17 +491,49 @@
          JSON.stringify(importParam),
          '\r\n'
        );
        // 2. 添加文件部分
        // 2. 添加changer参数部分 - API文档要求的必填参数
        console.log('手动构建请求体 - 添加changer参数:', formState.editor);
        parts.push(
          `--${boundary}\r\n`,
          `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}"\r\n`,
          'Content-Disposition: form-data; name="changer"\r\n',
          'Content-Type: text/plain\r\n',
          '\r\n',
          formState.editor,
          '\r\n'
        );
        // 尝试添加其他可能的参数名
        console.log('手动构建请求体 - 添加editorId参数:', formState.editor);
        parts.push(
          `--${boundary}\r\n`,
          'Content-Disposition: form-data; name="editorId"\r\n',
          'Content-Type: text/plain\r\n',
          '\r\n',
          formState.editor,
          '\r\n'
        );
        console.log('手动构建请求体 - 添加userId参数:', formState.editor);
        parts.push(
          `--${boundary}\r\n`,
          'Content-Disposition: form-data; name="userId"\r\n',
          'Content-Type: text/plain\r\n',
          '\r\n',
          formState.editor,
          '\r\n'
        );
        // 3. 添加文件部分
        parts.push(
          `--${boundary}\r\n`,
          `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}\r\n`,
          'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n',
          'Content-Transfer-Encoding: binary\r\n',
          '\r\n'
        );
        
        // 3. 读取文件内容
        // 4. 读取文件内容
        const fileReader = new FileReader();
        fileReader.onload = function(e) {
          if (!e.target?.result) {
@@ -512,7 +543,7 @@
          
          const fileContent = e.target.result;
          
          // 4. 添加文件内容和结束边界
          // 5. 添加文件内容和结束边界
          const allParts = [...parts];
          
          // 添加文件内容(使用 ArrayBuffer)
@@ -524,10 +555,10 @@
            `--${boundary}--\r\n`
          );
          
          // 5. 创建最终的请求体 Blob
          // 6. 创建最终的请求体 Blob
          const requestBody = new Blob(allParts, { type: `multipart/form-data; boundary=${boundary}` });
          
          // 6. 创建 XMLHttpRequest
          // 7. 创建 XMLHttpRequest
          const xhr = new XMLHttpRequest();
          
          // 监听请求完成
@@ -543,7 +574,12 @@
              }
            } else {
              console.error('请求失败:', xhr.status, xhr.statusText);
              reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
              try {
                const errorData = JSON.parse(xhr.responseText);
                reject(new Error(errorData.message || errorData.msg || `请求失败: ${xhr.status} ${xhr.statusText}`));
              } catch (e) {
                reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
              }
            }
          };
          
@@ -553,28 +589,18 @@
            reject(new Error('网络错误'));
          };
          
          // 监听请求进度(可选)
          xhr.upload.onprogress = function(event) {
            if (event.lengthComputable) {
              const percentComplete = (event.loaded / event.total) * 100;
              console.log('上传进度:', percentComplete.toFixed(2) + '%');
            }
          };
          // 打开请求
          xhr.open('POST', url, true);
          
          // 7. 设置请求头
          // 设置请求头
          xhr.setRequestHeader('X-Access-Token', token);
          xhr.setRequestHeader('X-Tenant-Id', '0');
          xhr.setRequestHeader('X-Version', 'v3');
          xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
          // 手动设置正确的 Content-Type,包含边界
          xhr.setRequestHeader('Content-Type', `multipart/form-data; boundary=${boundary}`);
          // 设置 Content-Length
          xhr.setRequestHeader('Content-Length', requestBody.size.toString());
          
          // 8. 发送请求
          // 发送请求
          xhr.send(requestBody);
        };
        
@@ -582,70 +608,36 @@
        fileReader.readAsArrayBuffer(fileObj);
      });
      
      console.log('后端响应数据:', responseData);
      console.log('后端响应:', responseData);
      
      const response = { data: responseData };
      // 9. 解析后端响应结果
      // 7. 处理响应
      if (responseData && responseData.success) {
        // 适配 JeecgBoot 后端返回格式
        importSummary.total = responseData.result?.total || responseData.data?.total || responseData.total || 0;
        importSummary.success = responseData.result?.success || responseData.data?.success || responseData.success || 0;
        importSummary.success = responseData.result?.success || responseData.data?.success || 0;
        importSummary.failed = responseData.result?.failed || responseData.data?.failed || responseData.failed || 0;
        importSummary.duration = responseData.result?.duration || responseData.data?.duration || 0;
        importErrors.value = responseData.result?.errors || responseData.data?.errors || responseData.errors || responseData.result?.errorList || responseData.data?.errorList || [];
        
        // 无错误但失败数大于0的兜底提示
        if (importErrors.value.length === 0 && importSummary.failed > 0) {
          importErrors.value = [{ key: 1, message: '部分记录导入失败,具体原因请查看后端日志或联系管理员' }];
          importErrors.value = [{ key: 1, message: '部分记录导入失败,请查看后端日志' }];
        }
        
        createMessage.success('批量导入处理完成');
        // 成功后返回上一级页面
        setTimeout(() => {
          router.back();
        }, 1000);
        // 成功时不显示结果,直接返回
        showResult.value = false;
      } else {
        // 导入失败处理
        importSummary.total = 0;
        importSummary.success = 0;
        importSummary.failed = 0;
        importSummary.duration = 0;
        importErrors.value = [{
          key: 1,
          message: responseData?.message || responseData?.msg || '导入失败,后端返回异常信息'
        }];
        createMessage.error(responseData?.message || responseData?.msg || '批量导入失败');
        // 失败时不显示结果
        showResult.value = false;
        const errorMsg = responseData?.message || responseData?.msg || '导入失败';
        importErrors.value = [{ key: 1, message: errorMsg }];
        createMessage.error(errorMsg);
      }
    } catch (error) {
      // 全局异常捕获
      console.error('导入过程中捕获异常:', error);
      // axios 错误响应格式:error.response.data 包含后端返回的错误信息
      console.error('导入异常:', error);
      const errorData = error.response?.data || error;
      const errorMessage = errorData?.message || errorData?.msg || error.message || '网络异常、文件无效或接口调用失败,请稍后重试';
      importSummary.total = 0;
      importSummary.success = 0;
      importSummary.failed = 0;
      importSummary.duration = 0;
      importErrors.value = [{
        key: 1,
        message: errorMessage
      }];
      showResult.value = false;
      createMessage.error(errorMessage);
      const errorMsg = errorData?.message || errorData?.msg || error.message || '网络异常';
      importErrors.value = [{ key: 1, message: errorMsg }];
      createMessage.error(errorMsg);
    } finally {
      // 确保无论成功失败,都关闭加载状态
      importLoading.value = false;
    }
  };
@@ -662,36 +654,79 @@
  // 获取文案编辑列表
  const getPersonList = async () => {
    try {
      const response = await defHttp.get({
        url: '/sys/user/userRoleList',
        params: {
          roleId: ROLE_ID_COPY_EDITOR,
        },
      });
      // 尝试多个可能的API端点获取文案编辑列表
      const apiEndpoints = [
        '/sys/user/userRoleList',
        '/sys/user/list',
        '/user/list'
      ];
      
      const records = normalizeContractRecords(response);
      const persons = records.map((item: any) => ({
        value: item.id || item.userId || '',
        label: item.realname || item.username || '未知用户'
      })).filter(item => item.value && item.label);
      let records = [];
      let apiSuccess = false;
      for (const endpoint of apiEndpoints) {
        try {
          console.log(`尝试API端点: ${endpoint}`);
          const response = await defHttp.get({
            url: endpoint,
            params: {
              roleId: ROLE_ID_COPY_EDITOR,
              pageNo: 1,
              pageSize: 100
            },
            timeout: 5000
          });
          records = normalizeContractRecords(response);
          if (records.length > 0) {
            apiSuccess = true;
            console.log(`API端点 ${endpoint} 成功返回 ${records.length} 条记录`);
            break;
          }
        } catch (error) {
          console.log(`API端点 ${endpoint} 调用失败:`, error);
        }
      }
      if (!apiSuccess) {
        createMessage.error('获取文案编辑列表失败');
        personList.value = [];
        return;
      }
      // 构建人员列表,使用ID作为value
      const persons = records.map((item: any) => {
        // 尝试获取ID
        const id = item.id || item.userId || item.userid || '';
        // 尝试获取姓名
        const name = item.realname || item.username || item.name || '未知用户';
        return {
          value: id, // 使用ID作为值
          label: name, // 只显示名字
          id: id
        };
      }).filter(item => item.value && item.label);
      
      personList.value = persons;
      console.log('获取文案编辑列表成功:', persons);
      console.log('原始记录数据:', records); // 输出原始记录数据,查看字段结构
      // 验证是否有手机号
      const hasPhone = persons.some(person => person.phone);
      console.log('是否包含手机号:', hasPhone);
      if (!hasPhone) {
        console.warn('警告: 文案编辑列表中没有手机号信息');
        // 尝试直接从原始记录中查找可能的手机号字段
        if (records.length > 0) {
          const firstRecord = records[0];
          console.log('第一条记录的所有字段:', Object.keys(firstRecord));
        }
      }
    } catch (error) {
      console.error('获取文案编辑列表失败:', error);
      createMessage.error('获取文案编辑列表失败,使用模拟数据');
      const mockPersons = [
        { value: '1', label: '测试用户' },
        { value: '2', label: '陈贵福' },
        { value: '3', label: '马春红' },
        { value: '4', label: '熊康利' },
        { value: '5', label: '小刘' },
        { value: '6', label: '小宋' },
        { value: '7', label: '郭永亮' },
        { value: '8', label: '张晓东' }
      ];
      personList.value = mockPersons;
      createMessage.error('获取文案编辑列表失败');
      personList.value = [];
    }
  };
  
src/views/demo/page/form/stepxieyi/Step3.vue
@@ -529,27 +529,47 @@
          const records = normalizeContractRecords(response);
          
          // 转换为下拉框选项格式
          const persons = records.map((item: any) => ({
            value: item.id || item.userId || '',
            label: item.realname || item.username || '未知用户'
          })).filter(item => item.value && item.label);
          const persons = records.map((item: any) => {
            // 尝试获取ID
            const id = item.id || item.userId || item.userid || '';
            // 尝试获取姓名
            const name = item.realname || item.username || item.name || '未知用户';
            return {
              value: id, // 使用ID作为值
              label: name, // 只显示名字
              id: id
            };
          }).filter(item => item.value && item.label);
          
          personList.value = persons;
          console.log('获取文案编辑列表成功:', persons);
          // 验证是否有ID
          const hasId = persons.some(person => person.id);
          console.log('是否包含ID:', hasId);
          if (!hasId) {
            console.warn('警告: 文案编辑列表中没有ID信息');
            // 尝试直接从原始记录中查找可能的ID字段
            if (records.length > 0) {
              const firstRecord = records[0];
              console.log('第一条记录的所有字段:', Object.keys(firstRecord));
            }
          }
        } catch (error) {
          console.error('获取文案编辑列表失败:', error);
          message.error('获取文案编辑列表失败,使用模拟数据');
          
          // 失败时使用模拟数据
          const mockPersons = [
            { value: '1', label: '测试用户' },
            { value: '2', label: '陈贵福' },
            { value: '3', label: '马春红' },
            { value: '4', label: '熊康利' },
            { value: '5', label: '小刘' },
            { value: '6', label: '小宋' },
            { value: '7', label: '郭永亮' },
            { value: '8', label: '张晓东' }
            { value: '1978763568401715202', label: '测试用户' },
            { value: '1978763568401715203', label: '陈贵福' },
            { value: '1978763568401715204', label: '马春红' },
            { value: '1978763568401715205', label: '熊康利' },
            { value: '1978763568401715206', label: '小刘' },
            { value: '1978763568401715207', label: '小宋' },
            { value: '1978763568401715208', label: '郭永亮' },
            { value: '1978763568401715209', label: '张晓东' }
          ];
          personList.value = mockPersons;
        }
@@ -692,7 +712,11 @@
            remark: "批量订单", // 备注信息写死为批量订单
            month: "12", // 合作月份写死为12个月
            price: "0", // 价格写死为0
            acceptindicator: "80" // 验收指标写死为80
            acceptindicator: "80", // 验收指标写死为80
            // 尝试在importParam中添加文案编辑参数
            changer: batchForm.editor,
            editorId: batchForm.editor,
            userId: batchForm.editor
          };
          console.log('业务参数:', importParam);
          
@@ -704,14 +728,38 @@
            return;
          }
          
          // 发送请求 - 使用 XMLHttpRequest 手动构建 multipart/form-data 请求
          const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL || 'http://192.168.31.222:8080/jeecg-boot';
          const url = `${domainUrl}/api/excel/importContract`;
          console.log('===== 手动构建 multipart/form-data 请求 =====');
          console.log('请求 URL:', url);
          // 发送请求 - 使用 defHttp 发送请求,利用现有的拦截器配置
          console.log('===== 使用 defHttp 发送请求 =====');
          console.log('文件对象:', fileObj);
          console.log('业务参数:', importParam);
          console.log('文案编辑ID:', batchForm.editor);
          // 使用 FormData API 构建请求体
          const formData = new FormData();
          // 添加文件
          formData.append('file', fileObj, fileObj.name);
          // 添加业务参数
          formData.append('importParam', JSON.stringify(importParam));
          // 添加changer参数 - API文档要求的必填参数
          formData.append('changer', batchForm.editor);
          console.log('添加changer参数:', batchForm.editor);
          // 验证 FormData 内容
          console.log('FormData 内容:');
          for (const [key, value] of formData.entries()) {
            console.log(`- ${key}: ${typeof value === 'string' ? value : value.name}`);
          }
          // 使用 XMLHttpRequest 手动构建请求,绕过拦截器问题
          console.log('使用 XMLHttpRequest 发送请求...');
          // 构建请求 URL
          const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL || 'http://192.168.31.222:8080/jeecg-boot';
          const url = `${domainUrl}/api/excel/importContract`;
          console.log('请求 URL:', url);
          
          // 手动构建 multipart/form-data 请求
          const responseData = await new Promise((resolve, reject) => {
@@ -730,17 +778,49 @@
              JSON.stringify(importParam),
              '\r\n'
            );
            // 2. 添加文件部分
            // 2. 添加changer参数部分 - API文档要求的必填参数
            console.log('手动构建请求体 - 添加changer参数:', batchForm.editor);
            parts.push(
              `--${boundary}\r\n`,
              `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}\r\n`,
              'Content-Disposition: form-data; name="changer"\r\n',
              'Content-Type: text/plain\r\n',
              '\r\n',
              batchForm.editor,
              '\r\n'
            );
            // 尝试添加其他可能的参数名
            console.log('手动构建请求体 - 添加editorId参数:', batchForm.editor);
            parts.push(
              `--${boundary}\r\n`,
              'Content-Disposition: form-data; name="editorId"\r\n',
              'Content-Type: text/plain\r\n',
              '\r\n',
              batchForm.editor,
              '\r\n'
            );
            console.log('手动构建请求体 - 添加userId参数:', batchForm.editor);
            parts.push(
              `--${boundary}\r\n`,
              'Content-Disposition: form-data; name="userId"\r\n',
              'Content-Type: text/plain\r\n',
              '\r\n',
              batchForm.editor,
              '\r\n'
            );
            // 3. 添加文件部分
            parts.push(
              `--${boundary}\r\n`,
              `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}"\r\n`,
              'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n',
              'Content-Transfer-Encoding: binary\r\n',
              '\r\n'
            );
            
            // 3. 读取文件内容
            // 4. 读取文件内容
            const fileReader = new FileReader();
            fileReader.onload = function(e) {
              if (!e.target?.result) {
@@ -750,7 +830,7 @@
              
              const fileContent = e.target.result;
              
              // 4. 添加文件内容和结束边界
              // 5. 添加文件内容和结束边界
              const allParts = [...parts];
              
              // 添加文件内容(使用 ArrayBuffer)
@@ -762,10 +842,10 @@
                `--${boundary}--\r\n`
              );
              
              // 5. 创建最终的请求体 Blob
              // 6. 创建最终的请求体 Blob
              const requestBody = new Blob(allParts, { type: `multipart/form-data; boundary=${boundary}` });
              
              // 6. 创建 XMLHttpRequest
              // 7. 创建 XMLHttpRequest
              const xhr = new XMLHttpRequest();
              
              // 监听请求完成
@@ -781,7 +861,12 @@
                  }
                } else {
                  console.error('请求失败:', xhr.status, xhr.statusText);
                  reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
                  try {
                    const errorData = JSON.parse(xhr.responseText);
                    reject(new Error(errorData.message || errorData.msg || `请求失败: ${xhr.status} ${xhr.statusText}`));
                  } catch (e) {
                    reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
                  }
                }
              };
              
@@ -801,8 +886,8 @@
              xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
              // 手动设置正确的 Content-Type,包含边界
              xhr.setRequestHeader('Content-Type', `multipart/form-data; boundary=${boundary}`);
              // 设置 Content-Length
              xhr.setRequestHeader('Content-Length', requestBody.size.toString());
              // 移除 Content-Length 设置,避免浏览器安全限制
              // xhr.setRequestHeader('Content-Length', requestBody.size.toString());
              
              // 发送请求
              xhr.send(requestBody);
@@ -818,7 +903,7 @@
          if (responseData && responseData.success) {
            // 适配 JeecgBoot 后端返回格式
            importSummary.total = responseData.result?.total || responseData.data?.total || responseData.total || 0;
            importSummary.success = responseData.result?.success || responseData.data?.success || responseData.success || 0;
            importSummary.success = responseData.result?.success || responseData.data?.success || 0;
            importSummary.failed = responseData.result?.failed || responseData.data?.failed || responseData.failed || 0;
            importSummary.duration = responseData.result?.duration || responseData.data?.duration || 0;
            importErrors.value = responseData.result?.errors || responseData.data?.errors || responseData.errors || responseData.result?.errorList || responseData.data?.errorList || [];