这段文本描述了一个系统开发中关于合同模板与业务合同之间附件处理的需求、思路、方案及实现过程。系统需要有一个合同模板页面(基础资料)和业务合同页面(单据),用户在业务合同页面选择合同模板时,自动将该模板的附件复制到业务合同页面的附件面板中。保存业务合同时,将单据上的部分字段数据填充到合同模板文件中,生成正式合同文件,并持久化到文件服务器上。 实现步骤包括:首先新建合同模板和业务合同页面,并放置相关控件;然后注册插件,在合同模板字段选择数据后,将基础资料的合同附件复制到单据的附件面板中;最后,在保存单据时,将单据数据填充到合同模板中生成正式合同文件,并保存到附件服务器上。文本中详细展示了如何通过编程实现这些功能,包括如何获取和处理附件数据、如何生成临时文件和正式文件等。
关键词:附件面板
一、需求
有一个合同模板页面(基础资料) & 业务合同页面(单据),且后者有一个合同模板字段(基础资料)用于选择前者维护的数据。用户在业务合同页面(单据)上的合同模板字段选择数据时,同步将该条基础资料数据的合同附件复制到单据上的合同附件(附件面板)中。保存时,将单据上部分字段的数据填充到前述文件中生成正式的合同文件,最后将其持久化到文件服务器上。
二、思路与方案
当我们在做系统开发,接到一个需求无从下手时,我们可以考虑先分析下平台本身功能的实现。该需求涉及附件面板控件开发,我们可先根据 苍穹如何根据url定位到映射的类,如何根据url追溯源码? 分析附件面板上传文件 & 持久化的过程,从而得出实现方案。
针对该需求,我们可分为两个主要步骤实现。第一,复制基础资料上的文件模板绑定到单据页面上。此时,我们只需分析清楚附件面板中数据的数据结构,再通过平台提供的工具类 kd.bos.servicehelper.AttachmentServiceHelper 即可实现。第二,保存单据时,向上一步新生成的合同文件中写入数据后持久化到文件服务器上即可。在这一步,向文件中写入业务数据,这个与苍穹开发无关,大家可自行查阅网上资料。其次,文件持久化,只要我们把附件面板数据按平台格式放入页面缓存中,用户在点击“保存”后,平台底层会自行将新合同文件持久化。
三、实现过程
1. 新建合同模板基础资料页面,并放置附件面板控件,其设计器界面如下图所示。
2. 新建业务合同单据页面,放置基础资料控件(合同模板)用于选择第 1 步页面维护的数据。
3. 在第 2 步创建的页面上注册插件实现功能:合同模板字段选择数据之后,同步将该基础资料数据的合同附件复制到单据上的合同附件(附件面板)中。
/** * * 业务逻辑:单据上的合同模板(基础资料)字段选择数据时,同步将该基础资料的合同附件复制到单据上的合同附件(附件面板)中 * */ @Override public void propertyChanged(PropertyChangedArgs e) { super.propertyChanged(e); String propertyName = e.getProperty().getName(); ChangeData changeData = e.getChangeSet()[0]; if (null != changeData) { if (StringUtils.equals(KEY_BILL_CONTRACTMODEL, propertyName)) { long contractId = (long) ((DynamicObject) changeData.getNewValue()).getPkValue(); this.copyContractAttachment(contractId); } } } /** * 复制勾选的基础资料(合同模板)保存到redis缓存中 * @param contractId 所选合同模板的主键ID */ private void copyContractAttachment(Object contractId) { // 查询所选合同模板(基础资料)的信息 List<Map<String, Object>> attachmentInfos = AttachmentServiceHelper.getAttachments(KEY_BASE_CONTRACTMODEL, contractId, KEY_BASE_CONTRACTMODELATTA); if (attachmentInfos.isEmpty()) { getView().showErrorNotification("该基础资料数据未上传合同模板文件!"); return; } // 默认每条合同模板(基础资料)数据只会有一个合同模板附件 Map<String, Object> attachmentInfo = attachmentInfos.get(0); String fileName = attachmentInfo.get("name").toString(); String originalUid = attachmentInfo.get("uid").toString(); String fileType = attachmentInfo.get("type").toString(); if (StringUtils.equals(fileType, "doc")) { fileType = "application/msword"; } else if (StringUtils.equals(fileType, "docx")) { fileType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; } // 从附件服务器中获取合同模板的输出流 AttachmentDto attachmentDto = AttachmentServiceHelper.getAttachmentInfoByAttPk(attachmentInfo.get("attPkId")); String url = attachmentDto.getResourcePath(); // 从附件服务器中拿到合同模板的输出流 ByteArrayOutputStream out = new ByteArrayOutputStream(); FileServiceFactory.getAttachmentFileService().download(url, out, null); // 将输出流转换为输入流 InputStream in = new ByteArrayInputStream(out.toByteArray()); // InputStream in = FileServiceFactory.getAttachmentFileService().getInputStream(url); logger.info("获取合同模板的文件流: " + in.getClass()); // 重新组装合同附件(附件面板)的数据 List<Map<String, Object>> contractAttaList = new ArrayList<Map<String, Object>>(); contractAttaList.add(this.newContractModelTempFile(fileName, fileType, in, originalUid)); WordUtils.close(in); // 将新生成的临时合同文件放入redis缓存中 Map<String, Object> attachementInfo = new HashMap<String, Object>(); attachementInfo.put(KEY_BILLATTAPANEL_CONTRACT, contractAttaList); IPageCache pageCache = getView().getPageCache(); pageCache.put("TampAttCache" + getView().getPageId(), SerializationUtils.toJsonString(attachementInfo)); getView().updateView(KEY_BILLATTAPANEL_CONTRACT); } /** * 生成新的临时文件 * @param fileName 文件名 * @param fileType 文件类型 * @param in 文件流 * @param originalUid 文件uid * @return */ private Map<String, Object> newContractModelTempFile (String fileName, String fileType, InputStream in, String originalUid) { Map<String, Object> contractMap = new HashMap<String, Object>(); contractMap.put("entityNum", KEY_BILL); // 当前时间戳 long time = new Date().getTime(); contractMap.put("createdate", time); // lastModified:时间戳 contractMap.put("lastModified", time); // name:文件名(含文件格式) contractMap.put("name", fileName); // size:文件大小 try { contractMap.put("size", in.available()); } catch (IOException e) { // ignore logger.info(e.getMessage()); } contractMap.put("status", "success"); // type:文件类型 contractMap.put("type", fileType); // uid StringBuffer uid = new StringBuffer(); uid.append("rc-upload-"); uid.append(time); uid.append("-"); String numberIndex = null; int index = originalUid.lastIndexOf("-"); if (index != -1) { numberIndex = originalUid.substring(index + 1); } uid.append(numberIndex); contractMap.put("uid", uid.toString()); // url:附件在附件服务器上的位置 StringBuffer newUrl = new StringBuffer(); newUrl.append(RequestContext.get().getClientFullContextPath()); if (!newUrl.toString().endsWith("/")) { newUrl.append("/"); } TempFileCache tempFileCache = CacheFactory.getCommonCacheFactory().getTempFileCache(); String tempUrl = tempFileCache.saveAsUrl(fileName, in, 2*60*60); newUrl.append(tempUrl); contractMap.put("url", newUrl); return contractMap; }
4. 在第 2 步创建的页面上注册插件实现功能:保存时,将单据上部分字段的数据填充到上一步新生成的临时合同文件中,以生成正式的合同文件,最后将其保存到附件服务器上。
/** * * 业务逻辑: 保存时,将单据上部分字段的数据填充到新生成的临时合同文件中,以生成正式的合同文件,最后将其保存到附件服务器上 * */ @Override public void beforeDoOperation(BeforeDoOperationEventArgs evt) { AbstractOperate operate = (AbstractOperate) evt.getSource(); String operationKey = operate.getOperateKey(); if (StringUtils.equalsIgnoreCase("save", operationKey)) { DynamicObject contract = (DynamicObject) getModel().getValue(KEY_BILL_CONTRACTMODEL); if (contract == null) { getView().showErrorNotification("请先选择合同模板!"); } this.generateRealContract(contract.getPkValue()); } } /** * 新生成的临时合同文件(word文件)填充单据数据后生成正式的合同文件 * @param contractId 所选合同模板的主键ID */ private void generateRealContract(Object contractId) { FormShowParameter formShowParameter = getView().getFormShowParameter(); OperationStatus operation = formShowParameter.getStatus(); logger.info("OperationStatus: " + operation); switch (operation) { case EDIT: // 如修改单据数据,则重新复制合同模板,再进行数据填充 this.copyContractAttachment(contractId); break; default: break; } // 从redis缓存中获取新生成的临时合同文件的输入流 IPageCache pageCache = getView().getPageCache(); String cacheJsonStr = pageCache.get("TampAttCache" + getView().getPageId()); Map<String, Object> attachementInfo = SerializationUtils.fromJsonString(cacheJsonStr, Map.class); List<Map<String, Object>> contractAttaList = null; if (attachementInfo != null) { contractAttaList = (List<Map<String, Object>>) attachementInfo.get(KEY_BILLATTAPANEL_CONTRACT); } if (contractAttaList != null && !contractAttaList.isEmpty()) { Map<String, Object> attachment = contractAttaList.get(0); String tempUrl = attachment.get("url").toString(); String fileName = attachment.get("name").toString(); // 新生成的临时合同文件的输入流 TempFileCache tempFileCache = CacheFactory.getCommonCacheFactory().getTempFileCache(); InputStream in = tempFileCache.getInputStream(tempUrl); logger.info("获取缓存中的文件流: " + in.getClass()); // 用单据上的数据替换合同模板中的字符串,得到正式合同的输入流 in = WordUtils.searchAndReplace(fileName, in, this.getContractData()); // 将生成的正式合同的输入流更新到redis缓存中 try { contractAttaList.get(0).put("size", in.available()); } catch (IOException e) { // ignore logger.info(e.getMessage()); } String tempNewUrl = tempFileCache.saveAsUrl(fileName, in, 2*60*60); StringBuffer newUrl = new StringBuffer(); newUrl.append(RequestContext.get().getClientFullContextPath()); if (!newUrl.toString().endsWith("/")) { newUrl.append("/"); } newUrl.append(tempNewUrl); contractAttaList.get(0).put("url", newUrl); attachementInfo.put(KEY_BILLATTAPANEL_CONTRACT, contractAttaList); pageCache.put("TampAttCache" + getView().getPageId(), SerializationUtils.toJsonString(attachementInfo)); switch (operation) { case EDIT: // 修改单据时,需将附件服务器上的旧正式合同文件删除掉 DynamicObject billObj = getModel().getDataEntity(); AttachmentPanel attachmentPanel = getView().getControl(KEY_BILLATTAPANEL_CONTRACT); List<Map<String, Object>> oldAttachments = attachmentPanel.getAttachmentData(); for (Map<String, Object> tempAtta : oldAttachments) { AttachmentServiceHelper.remove(KEY_BILL, billObj.getPkValue(), tempAtta.get("uid")); logger.info("删除单据 " + KEY_BILL + " (主键为: " + billObj.getPkValue() + " )的附件 " + tempAtta.get("attPkId")); } break; default: break; } getView().updateView(KEY_BILLATTAPANEL_CONTRACT); } }
四、效果图
操作步骤:在 案例云/案例应用-通用控件/附件面板/合同模板(基础资料)中录入数据,其中附件选择文章附件中样例doc文件,然后提交审核即可。打开 案例云/案例应用-通用控件/附件面板/附件面板样例1单据 录入数据,保存之后再下载单据附件即可查看效果。
合同模板基础资料页面上传的合同模板文件
填充业务单据数据后的正式合同文件
五、开发平台版本
不限
六、注意事项
1. 用户在给前端页面上的附件面板上传附件时,平台会先将附件数据放入页面缓存(亦即分布式缓存中)。用户点击“保存”按钮之后,平台底层在保存业务单据数据的同时,将缓存中的附件数据持久化到文件服务器上。
2. 涉及附件面板的开发,请先弄清楚其数据结构!
3. 开发该样例需引入jar包(poi-scratchpad-4.1.2.jar),附件中已包含,请自行下载!该jar包版本不可随意更换,需与平台底层引用的与poi相关的包保持一致!
4. 附件包中含有样例的页面元数据、Java插件源码。各位小伙伴通过在mc中升级补丁的方式导入元数据,在本地开发工具中导入Java插件,重启服务后即可复现样例效果。
七、参考资料
基础资料页面上的附件填充业务单据数据后绑定到单据页面上.ra …(36.90MB)
推荐阅读