需求背景:
云星空可以正常上传附件,由于有第三方系统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解码 → 合并文件 → 触发下载
```
七、错误处理机制
-
会话失效 : 提示用户重新登录
-
网络错误 : 显示错误信息并回退到备用方案
-
API调用失败 : 使用内置翻译映射表(翻译功能)或返回错误提示
八、扩展建议
-
断点续传 : 记录已下载位置,支持断点续传
-
进度显示 : 添加下载进度条
-
批量下载 : 压缩多个附件为ZIP包下载
-
文件预览 : 支持图片、PDF等文件在线预览
-
权限控制 : 添加文件访问权限验证