金碟云星空附件上传

需求背景:

云星空可以正常上传附件,由于有第三方系统PLM,计划取消金碟云星空基础资料维护,后期基础资料由PLM推送,包含表单和附件相关内容

需求目标:

PLM创建的基础资料,物料、BOM相关含附件即时同步推送到金碟云星空

前期准备:

金碟接口测试与接口逻辑

1、登录验证,获取sessionid

主要就是登录接口,获取接口对应的sessionid

2、上传文件

传sessionid,提交上传文件接口

3、绑定业务单据,调用保存接口

相关单据和文件参数

解决方案:

一、核心代码位置

1. 前端组件

物料附件监测页面: src/views/MaterialAttachment.vue

2. 后端路由

数据接口: backend/routes/data.js

二、附件上传核心代码

前端上传逻辑

复制代码
// 文件选择处理
const handleFileChange = async (event) => {
  const file = event.target.files[0];
  if (!file) return;
  
  const formData = new FormData();
  formData.append('file', file);
  formData.append('materialCode', searchForm.value.materialCode);
  
  try {
    const response = await fetch('/api/data/monitoring/material-attachment/upload', {
      method: 'POST',
      body: formData
    });
    
    const result = await response.json();
    if (result.success) {
      ElMessage.success('上传成功');
      await loadAttachmentList();
    } else {
      ElMessage.error(result.message || '上传失败');
    }
  } catch (error) {
    ElMessage.error('上传失败: ' + error.message);
  }
};

代码位置 : MaterialAttachment.vue

后端上传接口

复制代码
router.post('/monitoring/material-attachment/upload', async (req, res) => {
  try {
    // 处理文件上传
    const file = req.files?.file;
    const materialCode = req.body?.materialCode;
    
    if (!file || !materialCode) {
      return res.json({ success: false, message: '请选择文件并提供物料编码' });
    }
    
    // 生成存储路径
    const uploadDir = path.join(__dirname, '../uploads');
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true });
    }
    
    // 生成唯一文件名
    const ext = path.extname(file.name);
    const fileName = `${materialCode}-${Date.now()}${ext}`;
    const filePath = path.join(uploadDir, fileName);
    
    // 保存文件
    await file.mv(filePath);
    
    // 记录到数据库
    const saveQuery = `INSERT INTO A_JEKC_FILE_ATTACHMENT (FName, FPath, FType, FSize) 
                       VALUES (N'${fileName}', N'${filePath}', N'${ext}', ${file.size})`;
    await executeQuery(sqlServer, saveQuery);
    
    res.json({ success: true, message: '上传成功' });
  } catch (error) {
    res.json({ success: false, message: '上传失败: ' + error.message });
  }
});

代码位置 : data.js

三、附件下载核心代码

前端下载逻辑(使用 Kingdee AttachmentDownLoad API)

复制代码
// 下载单个附件
const handleDownload = async (attachment) => {
  const { fileId, fileName, materialCode } = attachment;
  const kdsvcSessionId = localStorage.getItem('kdsvcSessionId');
  
  if (!kdsvcSessionId) {
    ElMessage.error('会话信息缺失,请重新登录');
    return;
  }
  
  try {
    let startIndex = 0;
    const fileParts = [];
    let isLast = false;
    
    // 分块下载循环
    while (!isLast) {
      const response = await fetch('/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.AttachmentDownLoad.common.kdsvc', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'kdservice-sessionid': kdsvcSessionId
        },
        body: JSON.stringify({
          data: {
            FileId: fileId,
            StartIndex: startIndex
          }
        })
      });
      
      const data = await response.json();
      
      if (!data.Result?.ResponseStatus?.IsSuccess) {
        throw new Error(data.Result?.Message || '下载失败');
      }
      
      // Base64解码文件块
      const binaryData = atob(data.Result.FilePart);
      const uint8Array = new Uint8Array(binaryData.length);
      for (let i = 0; i < binaryData.length; i++) {
        uint8Array[i] = binaryData.charCodeAt(i);
      }
      fileParts.push(uint8Array);
      
      startIndex = data.Result.StartIndex;
      isLast = data.Result.IsLast;
    }
    
    // 合并文件块
    const totalLength = fileParts.reduce((sum, part) => sum + part.length, 0);
    const mergedArray = new Uint8Array(totalLength);
    let offset = 0;
    for (const part of fileParts) {
      mergedArray.set(part, offset);
      offset += part.length;
    }
    
    // 创建下载链接
    const blob = new Blob([mergedArray]);
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${materialCode}-${fileName}`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    
    ElMessage.success('下载成功');
  } catch (error) {
    ElMessage.error('下载失败: ' + error.message);
  }
};

代码位置 : MaterialAttachment.vue

四、API接口说明

上传接口

下载接口(Kingdee API)

请求URL : /K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.AttachmentDownLoad.common.kdsvc

请求参数 :

复制代码
{
  "data": {
    "FileId": "文件ID",
    "StartIndex": 0
  }
}

响应格式 :

复制代码
{
  "Result": {
    "ResponseStatus": {
      "IsSuccess": true,
      "Errors": [],
      "SuccessEntitys": [],
      "SuccessMessages": [],
      "MsgCode": 0
    },
    "StartIndex": 4194304,
    "IsLast": true,
    "FileSize": 3570547,
    "FileName": "8.png",
    "FilePart": "iVBORwggg==",
    "Message": ""
  }
}

五、开发说明

1. 分块下载原理

Kingdee K3 Cloud 的 AttachmentDownLoad API 采用分块下载机制:

  • 每次最大下载 :4MB(4194304字节)

  • StartIndex :从0开始,每次累加已下载字节数

  • IsLast :标记是否为最后一块

2. 关键技术点

Base64解码 :

复制代码
const binaryData = atob(data.Result.FilePart);
const uint8Array = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
  uint8Array[i] = binaryData.charCodeAt(i);
}

文件合并 :

复制代码
const totalLength = fileParts.reduce((sum, part) => sum + part.length, 0);
const mergedArray = new Uint8Array(totalLength);
let offset = 0;
for (const part of fileParts) {
  mergedArray.set(part, offset);
  offset += part.length;
}

3. 会话管理

下载需要有效的 kdservice-sessionid ,从 localStorage 获取:

复制代码
const kdsvcSessionId = localStorage.getItem('kdsvcSessionId');

4. 代理配置

在 vite.config.js 中配置了代理转发:填写实际的金蝶云星空服务器或者IP

复制代码
'/K3Cloud': {
  target: 'http://127.0.0.1',
  changeOrigin: true,
  rewrite: (path) => path.replace(/^\/K3Cloud/, '/K3Cloud')
}

5. 文件命名规则

下载文件命名格式: {物料编码}-{原始文件名}

六、流程示意图

```

用户选择文件 → 前端FormData上传 → 后端接收并保存 → 记录

到数据库

用户点击下载 → 获取SessionID → 分块请求Kingdee API

→ Base64解码 → 合并文件 → 触发下载

```

七、错误处理机制

  1. 会话失效 : 提示用户重新登录

  2. 网络错误 : 显示错误信息并回退到备用方案

  3. API调用失败 : 使用内置翻译映射表(翻译功能)或返回错误提示

八、扩展建议

  1. 断点续传 : 记录已下载位置,支持断点续传

  2. 进度显示 : 添加下载进度条

  3. 批量下载 : 压缩多个附件为ZIP包下载

  4. 文件预览 : 支持图片、PDF等文件在线预览

  5. 权限控制 : 添加文件访问权限验证

相关推荐
前端不太难1 天前
鸿蒙 App 的登录 / 订单 / 支付系统拆解
华为·状态模式·harmonyos
码码哈哈0.02 天前
基于 RSA 非对称加密与挑战码机制的前端登录安全方案
前端·安全·状态模式
前端不太难2 天前
鸿蒙 App 的 Task + State 双核心架构
架构·状态模式·harmonyos
lichenyang4532 天前
从零理解微前端:基于 React + Vite + qiankun 的子应用切换 Demo
前端·react.js·状态模式
yuzhiboyouye2 天前
web前端英语面试
前端·面试·状态模式
van久3 天前
Day27:菜单管理 + 动态路由(前端可直接用!)
前端·状态模式
huohuopro3 天前
Spring MVC 的核心知识点梳理
spring·mvc·状态模式
前端不太难3 天前
鸿蒙 App 的 Task 架构设计
华为·状态模式·harmonyos
Mr_pyx4 天前
你真的分得清 Spring、Spring MVC、Spring Boot 吗
状态模式