package org.springblade.mdm.program.service; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BizServiceImpl; import org.springblade.core.oss.OssTemplate; import org.springblade.core.oss.model.BladeFile; import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.FileUtil; import org.springblade.core.tool.utils.Func; import org.springblade.mdm.basesetting.machine.entity.Machine; import org.springblade.mdm.basesetting.machine.service.MachineService; import org.springblade.mdm.commons.contants.ZipConstants; import org.springblade.mdm.flow.entity.FlowProgramFile; import org.springblade.mdm.flow.service.CureFlowService; import org.springblade.mdm.flow.service.FlowCommonService; import org.springblade.mdm.program.entity.DncBackFile; import org.springblade.mdm.program.entity.NcNode; import org.springblade.mdm.program.entity.NcProgramExchange; import org.springblade.mdm.program.mapper.NcProgramExchangeMapper; import org.springblade.mdm.program.service.programannotation.*; import org.springblade.mdm.program.vo.DncSendBackData; import org.springblade.mdm.program.vo.DncSendBackFile; import org.springblade.mdm.utils.FileContentUtil; import org.springblade.mdm.utils.ZipTextFileContentUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Duration; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * DNC回传文件处理服务 * * @author yangys */ @Slf4j @Service public class DNCSendBackService extends BizServiceImpl { @Autowired private CureFlowService cureFlowService; @Autowired private NcNodeService ncNodeService; @Autowired private OssTemplate ossTemplate; @Autowired private BladeRedis bladeRedis; @Autowired private FlowCommonService flowCommonService; @Autowired private DncBackFileService dncBackFileService; @Autowired private MachineService machineService; /** * 偏离单文件末尾的模式:P+数字 */ private static final String P_NUMBER_PATTERN = "(?i)P\\d+"; private String getFileKey(){ return "dncimpfile-"+ AuthUtil.getUserId(); } /** * dnc回传文件上传 * @param file DNC回传文件 * @return 压缩包内程序包名的列表 */ public List dncSendBackUpload(MultipartFile file) { List list; if(file == null || file.isEmpty()){ throw new ServiceException("文件为空"); } if(!StringUtils.endsWith(file.getOriginalFilename(),".zip")){ throw new ServiceException("文件必须为zip包"); } try { BladeFile bfile = ossTemplate.putFile(file);//上传,供后续入库使用 //设置一个缓存,2小时过期 bladeRedis.setEx(getFileKey(),bfile.getName(), Duration.ofHours(2)); try(InputStream zipFileInputStream = ossTemplate.statFileStream(bfile.getName());) { list = parseProgramListFromZip(zipFileInputStream); } } catch (IOException e) { log.error("上传dnc回传文件失败",e); throw new ServiceException("解析DNC回传数据失败"); } return list; } /** * 从压缩包 解析回传程序列表,这里解析目录即可,目录就是程序包名 * @param inputStream 压缩包输入流 * @return 文件操作异常 */ List parseProgramListFromZip(InputStream inputStream) throws IOException { List result = null; ByteArrayInputStream byteInsStream = new ByteArrayInputStream(FileUtil.copyToByteArray(inputStream)); for (String encoding : ZipConstants.TRY_ENCODINGS) { try { result = parseProgramListByCharset(byteInsStream,Charset.forName(encoding)); log.error("使用编码 {} 解析成功 ",encoding); break; }catch (ServiceException se) { log.error("数据不正确异常:",se); break; } catch (Exception e) { byteInsStream.reset(); log.error("使用编码 {} 解析失败:",encoding,e); } } if(result != null) { return result; }else{ return Collections.emptyList(); } } /** * 从压缩包 解析回传程序列表,这里解析目录即可,目录就是程序包名 * @param inputStream 压缩包输入流 * @return 回传程序列表 * @throws IOException 文件操作异常 */ List parseProgramListByCharset(InputStream inputStream,Charset charset) throws IOException { List list = new ArrayList<>(); Path tempZipFile = createTempFile(inputStream); List fileEntryNameList = new ArrayList<>(); List dirEntryNameList = new ArrayList<>();//程序包名+工序版次 作为文件夹名 ZipEntry entry; try (ZipFile zipFile = new ZipFile(tempZipFile.toFile(),charset)) { Enumeration zipEntries = zipFile.entries(); //获取所有的entry名称 while (zipEntries.hasMoreElements()) { entry = zipEntries.nextElement(); if(entry.isDirectory()){ dirEntryNameList.add(entry.getName()); }else{ fileEntryNameList.add(entry.getName()); } } NcNode programPackageNode; //目录列表,即程序包列表 for(String entryName : dirEntryNameList){ DncSendBackData progData = new DncSendBackData(); String packageName = StringUtils.removeEnd(entryName,"/"); Optional optFilename = fileEntryNameList.stream().filter(n -> n.startsWith(entryName)).findFirst(); if(optFilename.isPresent()){ entry = zipFile.getEntry(optFilename.get()); InputStream ins = zipFile.getInputStream(entry); ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(IOUtils.toByteArray(ins)); //解析机床 progData.setFileBackTime(DateUtil.fromInstant(entry.getLastModifiedTime().toInstant())); AnnotationProperties defAnnoProperties =AnnotationProperties.getDefault(); String statusLine = FileContentUtil.readLineAt(byteArrayIns,defAnnoProperties.getStatusLineIndex());//状态注释行 byteArrayIns.reset(); String sendPathLine = FileContentUtil.readLineAt(byteArrayIns,defAnnoProperties.getSendPathLineIndex());//状态注释行 byteArrayIns.reset(); if(statusLine.contains(AnnotationUtil.GH)){ //固化,不应回传,忽略 log.warn("状态{},不应回传,忽略",statusLine); continue; } Machine machine = this.machineService.getMachineBySendPathAnnotation(sendPathLine); if(machine == null){ throw new ServiceException("根据下发路径未找到程序对应的机床:"+sendPathLine); } progData.setProgramName(packageName); if(statusLine.contains(AnnotationUtil.SQ)){ //试切 programPackageNode = ncNodeService.getLastEditionTryingProgramPackage(packageName);//TODO 还需根据机床组(如何获取?根据下发路径获取机床,进而获取),,processEdition }else if(statusLine.contains(AnnotationUtil.LG)){ //临时更改单 programPackageNode =ncNodeService.getLastEditionDeviationProgramPackage(packageName); }else{ throw new ServiceException("状态注释不在范围内:"+statusLine+",仅试切、临时更改单可以回传"); } if(programPackageNode != null) { progData.setId(programPackageNode.getId()); progData.setProgramNo(programPackageNode.getProgramNo()); List fileEntryNames = fileEntryNameList.stream().filter(n -> n.startsWith(packageName)).toList(); List programFiles = new ArrayList<>(); fileEntryNames.forEach( filePath ->{ DncSendBackFile backFile = new DncSendBackFile(); backFile.setEntryName(filePath); backFile.setName(StringUtils.removeStart(filePath,entryName)); programFiles.add(backFile); }); progData.setFiles(programFiles); list.add(progData); }else{ throw new ServiceException("找不到程序包名:"+packageName); } }else{ throw new ServiceException(entryName+"包下未找到文件"); } } } return list; } /** * 入库回传文件,并启动固化流程 * @param ids id列表逗号分隔,程序包名 节点的id */ @Transactional public void dncFileAccept(String ids) throws IOException { List acceptIdList = Func.toLongList(ids); String filekey = getFileKey(); String zipFileName = bladeRedis.get(filekey); log.info("filekey={},文件名={}",filekey,zipFileName); Map> pkgIdFileMap = dealWithBackFile(zipFileName,acceptIdList); cureFlowService.startCureNew(pkgIdFileMap); } /** * 处理回传文件 * @param ossFileName * @param acceptIdList * @return * @throws IOException */ private Map> dealWithBackFile(String ossFileName, List acceptIdList) throws IOException{ Map> pkgIdFileMap = new HashMap<>(); //ByteArrayInputStream byteInsStream = new ByteArrayInputStream(FileUtil.copyToByteArray(inputStream)); for (String encoding : ZipConstants.TRY_ENCODINGS) { try { pkgIdFileMap = dealWithBackFileWithCharset(ossFileName,acceptIdList,Charset.forName(encoding)); log.error("使用编码 {} 解析成功 ",encoding); break; } catch (Exception e) { //byteInsStream.reset(); log.error("使用编码 {} 解析失败: ",encoding,e); } } return pkgIdFileMap; } /** * 处理回传文件 * @param ossFileName * @param acceptIdList * @return * @throws IOException */ private Map> dealWithBackFileWithCharset(String ossFileName, List acceptIdList,Charset charset) throws IOException{ Map> pkgIdFileMap = new HashMap<>(); InputStream inputStream = this.ossTemplate.statFileStream(ossFileName); Path tempZipFile = createTempFile(inputStream); List entryNameList = new ArrayList<>(); ZipEntry entry; try (java.util.zip.ZipFile zipFile = new java.util.zip.ZipFile(tempZipFile.toFile(),charset)) { Enumeration entries = zipFile.entries(); while(entries.hasMoreElements()) { entry = entries.nextElement(); entryNameList.add(entry.getName()); } log.info("allentrynames:{}",entryNameList); List allAcceptPackages = this.ncNodeService.lambdaQuery().in(NcNode::getId,acceptIdList).list(); //根据内部文件,读取和分析程序包和程序文件数据 List dirList = entryNameList.stream().filter(s -> s.endsWith("/")).toList(); for(String dir : dirList){ String programPackageName = StringUtils.removeEnd(dir,"/"); Optional optPackageNode = allAcceptPackages.stream().filter(node -> StringUtils.equals(node.getName(),programPackageName)).findFirst(); if(optPackageNode.isEmpty()){ throw new ServiceException("找不到程序"+programPackageName); } NcNode packageNode = optPackageNode.get(); if(packageNode.hasCured()) { throw new ServiceException(programPackageName + "已经固化,请勿重复入库。"); } //偏离程序判断是否重复回传 if(packageNode.isDeviationProgram() && packageNode.hasLocked()) { throw new ServiceException(programPackageName + "已锁定的程序不可以再次回传。"); } //检查是否在审批过程中 boolean active = flowCommonService.isProcessInstanceActive(packageNode.getProcessInstanceId()); if(active){ throw new ServiceException(programPackageName+"正在审批中,请勿等待审批完成。"); } //验证都过了,保存dncbackFile DncBackFile backFile = new DncBackFile(); backFile.setNcNodeId(packageNode.getId()); backFile.setOssName(ossFileName); dncBackFileService.save(backFile); List flowFiles = new ArrayList<>(); //查找包下的文件数据, entryNameList.stream().filter(s -> s.startsWith(dir)).forEach(entryName -> { log.info("{}下的文件:{}",dir,entryName); if(!entryName.endsWith("/")){ //实际的文件 String fileName = StringUtils.removeStart(entryName,dir);//去除文件名路径部分 fileName = removeDeviationPart(fileName); try { FlowProgramFile newFlowFile = new FlowProgramFile(); newFlowFile.setProgramName(packageNode.getName()); newFlowFile.setProcessInstanceId(null);//先置为空,启动流程后设置该值 newFlowFile.setFileType(FlowProgramFile.TYPE_PROGRAM); newFlowFile.setName(fileName); InputStream ins = zipFile.getInputStream(zipFile.getEntry(entryName)); BladeFile newOssFile = ossTemplate.putFile("mdm",fileName,ins); newFlowFile.setOssName(newOssFile.getName()); flowFiles.add(newFlowFile); } catch (IOException e) { throw new RuntimeException(e); } } }); pkgIdFileMap.put(packageNode.getId(),flowFiles); } } return pkgIdFileMap; } String removeDeviationPart(String filename){ String finalFilename = filename; //去掉文件名中可能带有的偏离单部分:-P[序号] String ext = FilenameUtils.getExtension(filename); String dotExt = StringUtils.isNotBlank(ext)?"."+ext:ext;//带点的扩展名 String notExtName = StringUtils.removeEnd(filename,dotExt); int idx = notExtName.lastIndexOf("-"); if(idx != -1){ String endPart = notExtName.substring(idx+1); //Pattern.CASE_INSENSITIVE boolean containsPld = endPart.matches(P_NUMBER_PATTERN); if(containsPld){ finalFilename = notExtName.substring(0, idx)+dotExt; } } return finalFilename; } /** * 创建一个临时zip文件 * @param inputStream 文件的输入流 * @return path 文件 * @throws IOException */ Path createTempFile(InputStream inputStream) throws IOException { byte[] zipData = FileUtil.copyToByteArray(inputStream); Path tempFile = Files.createTempFile("tempzip"+System.currentTimeMillis(), ".zip"); // 写入字节数据到临时文件 Files.write(tempFile, zipData, StandardOpenOption.WRITE); return tempFile; } /** * 获取回传文件的内容 * @param entryName 文件在压缩包内的路径 * @return 文件内容文本 */ public String getEntryFileContent(String entryName) throws IOException { String result = ""; String zipFileName = bladeRedis.get(getFileKey()); return ZipTextFileContentUtil.getTextContent(this.ossTemplate.statFileStream(zipFileName),entryName); } } @Data class PackageAndProcessEdition{ private String programPackageName; private String processEdition; public String programName(){ return programPackageName+"-"+processEdition; } }