如何在单据列表批量上传附件
1 业务背景
单据上传附件是一个比较普遍的业务场景。目前标准产品仅支持,打开单据上传附件,并且大多数业务默认设置只有单据在暂存状态时才能上传附件。特殊情况下,业务人员需要一次性在多个单据上传多个附件,这些附件可能有一部分是重复的文件,这样的重复操作既浪费了业务人员的时间也浪费了文件服务器的空间。此二开案例可以解决这种特殊的业务场景。
2 解决方案
首先开发一个动态表单,可基于内容弹窗模板创建,新建后调整为合适的宽高度,然后在表单里增加一个附件面板,增加表单插件,用于处理父页面传递的数据以及执行附件上传的业务逻辑。其次在需要进行批量上传附件的业务单据的列表上配置对于操作按钮弹出该动态表单进行业务操作。
添加面板后需要修改默认的两个操作按钮:取消和确认,在此动态表单里新增两个空操作(donothing)以替换原始的按钮,清空(标识:clean)以及确定(标识:upload),配置完操作后就可以开始编写表单插件了,表单大致结构如下:
下面开始设计表单插件,首先设置按钮相关操作逻辑:确定和清空。需要注意,点击确认时附件可能没有上传完毕,并且首次上传到附件面板的数据都是存放在临时文件服务器的,附件的url含tempfile关键字,对于正在上传和上传文件过期的情况需要优先判断。
// 静态常量 private static final Log log = LogFactory.getLog(UploadAtt2BillsFormPlugin.class); public static final String ATTACH_KEY = "inja_attachmentpanel"; // 动态表单的附件面板标识 - 根据情况修改
@Override public void afterDoOperation(AfterDoOperationEventArgs args) { if (this.hasAttachmentUploading()) { this.getView().showTipNotification(ResManager.LoadKDString("附件上传中,请稍后再试。", "UploadAtt2BillsFormPlugin_1")); return; } String operateKey = args.getOperateKey(); if ("upload".equals(operateKey)) { List<DynamicObject> timeOutAttList = AttachmentFieldServiceHelper.getTimeOutAttList(this.getView().getPageId()); if (!CollectionUtils.isEmpty(timeOutAttList)) { StringBuilder timeoutMessage = new StringBuilder(ResManager.LoadKDString("临时附件已超时,请重新上传以下文件:\r\n", "UploadAtt2BillsFormPlugin_2")); for(DynamicObject attDynamicObj : timeOutAttList) { timeoutMessage.append(attDynamicObj.getLocaleString("name").getLocaleValue()).append("\r\n"); } this.getView().showConfirm(timeoutMessage.toString(), MessageBoxOptions.OK); return; } this.doUpload(); } else if ("clean".equals(operateKey)) { AttachmentPanel attachmentPanel = getControl(ATTACH_KEY); List<Map<String, Object>> attachmentData = attachmentPanel.getAttachmentData(); if (attachmentData.size() > 0) { this.getView().showConfirm(ResManager.LoadKDString("是否清空当前附件面板?", "UploadAtt2BillsFormPlugin_3"), MessageBoxOptions.YesNo, new ConfirmCallBackListener("clean_callback", this)); } else { this.getView().showTipNotification(ResManager.LoadKDString("附件面板已清空!", "UploadAtt2BillsFormPlugin_4")); } } } private boolean hasAttachmentUploading() { IPageCache cache = this.getView().getService(IPageCache.class); String uploadingAttJson = cache.get("UploadingAtt" + this.getView().getPageId()); return StringUtils.isNotBlank(uploadingAttJson); }
其中 this.doUpload() 方法如下,大致流程是对已经上传为临时文件的附件进行遍历,依次进行临时文件持久化以及绑定单据关联的操作,并且进行了适当的异常处理,对于正常上传并绑定单据的附件会直接删除,出现异常的文件会保留在面板上。其中注意动态表单的自定义参数 formData 要传递单据实体相关数据信息,需要进行JSON序列化为字符串,在单据列表插件触发弹窗时进行传递。
private void doUpload() { AttachmentPanel attachmentPanel = getControl(ATTACH_KEY); List<Map<String, Object>> attachmentData = attachmentPanel.getAttachmentData(); if (attachmentData.size() == 0) { this.getView().showTipNotification(ResManager.LoadKDString("请先上传文件。", "UploadAtt2BillsFormPlugin_8")); return; } FormShowParameter showParameter = this.getView().getFormShowParameter(); Map<String, Object> params = showParameter.getCustomParams(); String formData = (String) params.get("formData"); if (StringUtils.isBlank(formData)) { // todo: 表单直接预览 this.getView().showTipNotification("formData is blank !"); return; } List<Map<String, Object>> formDataList = SerializationUtils.fromJsonString(formData, List.class); List<Map<String, Object>> formDataErr = new ArrayList<>(); boolean hasAttachmentDataUploadedErr = false, hasFormDataErr = false; ORM orm = ORM.create(); List<DynamicObject> attBillRelList = new ArrayList<>(); List<Map<String, Object>> attachmentDataUploaded = new ArrayList<>(); for (Map<String, Object> attachment : attachmentData) { // 临时文件持久化 String path = this.uploadFileServer(attachment); if (StringUtils.isBlank(path)) { hasAttachmentDataUploadedErr = true; continue; } Map<String, Object> map = new HashMap<>(attachment); map.put("url", path); attachmentDataUploaded.add(map); attBillRelList.clear(); // 保存所有单据附件关联,必要:entityNumber,billId,attKey for (Map<String, Object> data : formDataList) { String entityNumber = (String) data.get("entityNumber"); // 单据实体编码 Integer rowKey = (Integer) data.get("rowKey"); // 列表行序号 Object billId = data.get("billId"); // 单据主键 String billNo = (String) data.get("billNo"); // 单据编码 String attKey = (String) data.get("attKey"); // 单据面板标识,eg."attachmentpanel" try { attBillRelList.addAll(this.genAttachmentRel(entityNumber, billId+"", attKey, attachmentDataUploaded, orm)); } catch (Exception e){ hasFormDataErr = true; log.error(String.format("genAttachmentRel[rowKey:%s, entityNumber:%s, billId:%s, billNo:%s], err: %s", entityNumber, rowKey, billId, billNo, e.getMessage())); formDataErr.add(data); } } if (formDataErr.isEmpty()){ // 删除面板附件及临时文件 attachmentPanel.remove(attachment); } else { List<Object> billNos = formDataErr.stream().map(e -> e.get("billNo")).collect(Collectors.toList()); log.error(String.format("附件[%s],%s条单据绑定附件数据失败:\r\n%s", attachment, billNos.size(), billNos)); formDataErr.clear(); } attachmentDataUploaded.clear(); SaveServiceHelper.save(attBillRelList.toArray(new DynamicObject[0])); } if (!hasAttachmentDataUploadedErr && !hasFormDataErr) { this.getView().showSuccessNotification(ResManager.LoadKDString("上传成功!", "UploadAtt2BillsFormPlugin_5")); } else if (hasAttachmentDataUploadedErr && !hasFormDataErr) { this.getView().showTipNotification(ResManager.LoadKDString("以下附件上传失败,请重新上传文件!", "UploadAtt2BillsFormPlugin_6")); } else { this.getView().showErrorNotification(ResManager.LoadKDString("附件上传出现未知异常,请联系管理员查询日志分析!", "UploadAtt2BillsFormPlugin_7")); } }
其中两个关键方法 this.uploadFileServer(attachment) 和 this.genAttachmentRel(entityNumber, billId+"", attKey, attachmentDataUploaded, orm),前者用于持久化附件,后者获取附件面板实体数据以记录附件和单据的关联。
/** * 保存临时文件到文件服务器进行持久化 * @see AttachmentServiceHelper#saveTempToFileService * @param attDataItem 已上传的附件临时文件Map信息 * @return 上传文件服务器返回url */ private String uploadFileServer(Map<String, Object> attDataItem) { try{ FileService fs = FileServiceFactory.getAttachmentFileService(); RequestContext requestContext = RequestContext.get(); TempFileCache fileCache = CacheFactory.getCommonCacheFactory().getTempFileCache(); String filename = (String) attDataItem.get("name"); String tempUrl = (String) attDataItem.get("url"); String uuid = UUID.randomUUID().toString().replace("-", ""); String filepath = FileNameUtils.getAttachmentFileName( // 此处 attachmentpanel 用于文件路径的唯一标识 requestContext.getTenantId(), requestContext.getAccountId(), "attachmentpanel", uuid + "/" + filename); InputStream inputStream = fileCache.getInputStream(tempUrl); FileItem item = new FileItem(filename, filepath, inputStream); String[] splits = filename.trim().split("\\."); String fileType = splits[splits.length - 1]; long compressPicSize = 0L; int fileSize = inputStream.available() / 1024; if ("jpg,jpeg,png,gif,bmp,tiff,tga,ico,dib,rle,emf,jpe,jfif,pcx,dcx,pic,tif,wmf".contains(fileType.toLowerCase())) { compressPicSize = AttachmentServiceHelper.getCompressPicSize(); } if (compressPicSize != 0L && fileSize > compressPicSize) { return fs.compressPicUpload(item, compressPicSize); } else { return fs.upload(item); } } catch (Exception e){ log.error("uploadFileServer err: " + e.getMessage()); return null; } } /** * 绑定附件到单据的附件面板 * @see AttachmentServiceHelper#upload * @param entityNumber 实体编码 * @param billPkId 单据主键 * @param attList 附件信息 * @param orm orm实例 * @return 附件面板实体数据,用于入库记录 */ private DynamicObjectCollection genAttachmentRel(String entityNumber, String billPkId, String attachKey, List<Map<String, Object>> attList, ORM orm){ DynamicObjectType entityType = (DynamicObjectType)orm.getDataEntityType("bos_attachment"); DynamicObjectCollection dynColl = new DynamicObjectCollection(entityType, null); if (attList == null || attList.size() == 0) { return dynColl; } long[] ids = orm.genLongIds(entityType, attList.size()); Date today = new Date(); for(int i = 0; i < attList.size(); i++) { Map<String, Object> attach = attList.get(i); DynamicObject dynamicObject = new DynamicObject(entityType); dynamicObject.set("id", ids[i]); dynamicObject.set("FNUMBER", attach.get("uid")); dynamicObject.set("FBillType", entityNumber); dynamicObject.set("FInterID", billPkId); Object lastModified = attach.get("lastModified"); if (lastModified instanceof Date) { dynamicObject.set("FModifyTime", lastModified); } else if (lastModified instanceof Long) { dynamicObject.set("FModifyTime", new Date((Long)lastModified)); } else { dynamicObject.set("FModifyTime", today); } dynamicObject.set("fcreatetime", attach.getOrDefault("uploadTime", today)); String name = (String)attach.get("name"); dynamicObject.set("FaliasFileName", name); dynamicObject.set("FAttachmentName", name); String extName = name != null ? name.substring(name.lastIndexOf(46) + 1) : ""; dynamicObject.set("FExtName", extName); long compressPicSize = AttachmentServiceHelper.getCompressPicSize(); if ("jpg,jpeg,png,gif,bmp,tiff,tga,ico,dib,rle,emf,jpe,jfif,pcx,dcx,pic,tif,wmf".contains(extName.toLowerCase()) && compressPicSize != 0L && Long.parseLong(attach.get("size").toString()) > compressPicSize * 1024L) { dynamicObject.set("FATTACHMENTSIZE", compressPicSize * 1024L); } else { dynamicObject.set("FATTACHMENTSIZE", attach.get("size")); } dynamicObject.set("FFileId", attach.get("url")); dynamicObject.set("FCREATEMEN", RequestContext.get().getCurrUserId()); dynamicObject.set("fattachmentpanel", attachKey); dynamicObject.set("filesource", attach.get("filesource")); if (attach.containsKey("description")) { dynamicObject.set("fdescription", attach.get("description")); } dynColl.add(dynamicObject); } return dynColl; }
另外关闭窗口以及在操作中一部分回调确认的逻辑如下:
@Override public void beforeClosed(BeforeClosedEvent e) { super.beforeClosed(e); AttachmentPanel attachmentPanel = getControl(ATTACH_KEY); List<Map<String, Object>> attachmentData = attachmentPanel.getAttachmentData(); if (attachmentData.size() > 0) { e.setCancel(true); this.getView().showConfirm(ResManager.LoadKDString("是否放弃已上传的附件?", "UploadAtt2BillsFormPlugin_0"), MessageBoxOptions.YesNo, new ConfirmCallBackListener("cancel_callback", this)); } } @Override public void confirmCallBack(MessageBoxClosedEvent evt) { switch(evt.getCallBackId()){ case "cancel_callback": if (MessageBoxResult.Yes.equals(evt.getResult())) { this.cleanAllAttach(); this.getView().close(); } break; case "clean_callback": if (MessageBoxResult.Yes.equals(evt.getResult())) { this.cleanAllAttach(); } break; } } private void cleanAllAttach() { AttachmentPanel attachmentPanel = getControl(ATTACH_KEY); List<Map<String, Object>> attachmentData = attachmentPanel.getAttachmentData(); for (Map<String, Object> data : attachmentData) { attachmentPanel.remove(data); } }
批量上传的表单设计好后,再到需要支持批量上传的单据列表配置对应的列表插件,设置相关的按钮操作触发即可,如下图,设置一个 上传附件(标识:uploadbills)的操作按钮。
然后配置对应的列表插件,主要逻辑由 uploadbills 操作触发,代码如下,根据需要配置 entityNumber 和 attKey 等参数,名称与一开始设计的动态表单插件里相关标识相匹配即可,可根据实际情况灵活处理。
@Override public void afterDoOperation(AfterDoOperationEventArgs args) { switch (args.getOperateKey()) { case "uploadbills": FormShowParameter showParameter = new FormShowParameter(); showParameter.setFormId("inja_uploadatt2bills"); showParameter.getOpenStyle().setShowType(ShowType.Modal); List<Map<String, Object>> formData = new ArrayList<>(); for (ListSelectedRow row : getSelectedRows()) { Map<String, Object> data = new HashMap<>(); data.put("entityNumber", "inja_attachment_test"); // 当前单据实体编码 data.put("attKey", "attachmentpanel"); // 当前单据面板标识 data.put("rowKey", row.getRowKey()); data.put("billId", row.getPrimaryKeyValue()); data.put("billNo", row.getBillNo()); formData.add(data); } showParameter.getCustomParams().put("formData", SerializationUtils.toJsonString(formData)); this.getView().showForm(showParameter); break; } }
具体插件代码可在附件 查看,效果演示如下。在单据列表选择多个单据:
点击附件上传按钮,选择文件上传到动态表单的附件面板:
点击确定后提示附件上传成功:
打开单据查看,附件已经成功上传到多个单据,删除以及其他操作可以正常执行,不同单据的附件在服务器也只保存了一份文件,节省了硬盘空间。
3 注意事项
此代码在动态表单接受参数时没有对参数进行校验,错误传参会导致关联表出现脏数据,可在传参之前进行校验
附件上传时没有进行权限检查以及单据状态判断,建议开放给管理员使用或单独配置权限判断
后台绑定附件会越过原始单据附件面板的元数据限制条件,例如如果单据的附件面板绑定了附件数字段,后台方法上传附件无法更新附件数,可根据实际情况进行补偿处理
考虑到文件IO操作不方便回滚,代码在保存附件关联表时,是在每个文件上传后分别保存,当面板附件过多时可能出现性能问题
各个单据的面板重复的文件删除后只影响当前单据面板的数据,当所有单据都没有该文件的关联记录时,文件才会从服务器删除
4 环境版本
金蝶云·苍穹 6.0
5 相关资料
如何在单据列表批量上传附件
本文2024-09-23 00:36:38发表“云苍穹知识”栏目。
本文链接:https://wenku.my7c.com/article/kingdee-cangqiong-140575.html