From e6f1aed780016b18a9701fba33a9e3455a11b5a1 Mon Sep 17 00:00:00 2001 From: huguanghan Date: Tue, 24 Dec 2024 15:14:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=9B=BE=E7=89=87=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/EquipmentController.java | 4 +- .../equipment/entity/SysEquipmentDocs.java | 8 +- .../service/SysEquipmentService.java | 2 +- .../service/impl/SysEquipmentServiceImpl.java | 148 +++++++++++++----- .../fdr/service/MinioViewsServcie.java | 63 +++++--- das/src/main/resources/application.yml | 2 +- 6 files changed, 163 insertions(+), 64 deletions(-) diff --git a/das/src/main/java/com/das/modules/equipment/controller/EquipmentController.java b/das/src/main/java/com/das/modules/equipment/controller/EquipmentController.java index 7fa6a9f7..bc38a7c9 100644 --- a/das/src/main/java/com/das/modules/equipment/controller/EquipmentController.java +++ b/das/src/main/java/com/das/modules/equipment/controller/EquipmentController.java @@ -196,8 +196,8 @@ public class EquipmentController { } @RequestMapping(value = "/file/upload", method = RequestMethod.POST) - public R addFile(Long deviceId, String folderName, MultipartFile file) { - SysEquipmentDocs upload = sysEquipmentService.upload(deviceId, folderName, file); + public R> addFile(Long deviceId, String component,String folderName, List fileList) throws Exception { + List upload = sysEquipmentService.upload(deviceId, component,folderName, fileList); return R.success(upload); } } diff --git a/das/src/main/java/com/das/modules/equipment/entity/SysEquipmentDocs.java b/das/src/main/java/com/das/modules/equipment/entity/SysEquipmentDocs.java index e4f3096f..ced4545c 100644 --- a/das/src/main/java/com/das/modules/equipment/entity/SysEquipmentDocs.java +++ b/das/src/main/java/com/das/modules/equipment/entity/SysEquipmentDocs.java @@ -17,12 +17,18 @@ import java.util.Date; @AllArgsConstructor public class SysEquipmentDocs { - @TableId(value = "deviceid") + @TableId(value = "id") + private Long id; + + @TableField(value = "deviceid") private Long deviceId; @TableField(value = "name") private String name; + @TableField(value = "component") + private String component; + @TableField(value = "url") private String url; /** diff --git a/das/src/main/java/com/das/modules/equipment/service/SysEquipmentService.java b/das/src/main/java/com/das/modules/equipment/service/SysEquipmentService.java index b85657e9..29ac88ca 100644 --- a/das/src/main/java/com/das/modules/equipment/service/SysEquipmentService.java +++ b/das/src/main/java/com/das/modules/equipment/service/SysEquipmentService.java @@ -39,5 +39,5 @@ public interface SysEquipmentService { SysGenExtProps querySysEquipmentExtProps(Long id); - SysEquipmentDocs upload(Long deviceId, String folderName, MultipartFile file); + List upload(Long deviceId, String component,String folderName, List fileList) throws Exception; } diff --git a/das/src/main/java/com/das/modules/equipment/service/impl/SysEquipmentServiceImpl.java b/das/src/main/java/com/das/modules/equipment/service/impl/SysEquipmentServiceImpl.java index bcffb085..6c216ad9 100644 --- a/das/src/main/java/com/das/modules/equipment/service/impl/SysEquipmentServiceImpl.java +++ b/das/src/main/java/com/das/modules/equipment/service/impl/SysEquipmentServiceImpl.java @@ -1,6 +1,7 @@ package com.das.modules.equipment.service.impl; import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.img.ImgUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.poi.excel.ExcelReader; @@ -41,6 +42,7 @@ import jakarta.annotation.Resource; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.poi.ss.usermodel.HorizontalAlignment; import org.apache.poi.ss.usermodel.VerticalAlignment; @@ -50,6 +52,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; @@ -64,6 +70,7 @@ import java.util.List; @Transactional(rollbackFor = Exception.class) @Service +@Slf4j public class SysEquipmentServiceImpl implements SysEquipmentService { @Autowired @@ -98,6 +105,10 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { @Autowired private SysEquipmentDocsMapper sysEquipmentDocsMapper; + // 7厘米转换为像素(以300 DPI为基准) + private static final int TARGET_WIDTH = 826; // 7厘米 * 300 DPI / 2.54 + private static final int TARGET_HEIGHT = 826; // 7厘米 * 300 DPI / 2.54 + @Override public SysEquipmentVo creatSysEquipment(SysEquipmentDto sysEquipmentDto) { @@ -118,8 +129,8 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { sysEquipment.setRevision(1); sysEquipmentMapper.insert(sysEquipment); //物模型不为空 增加设备物模型mapping缓存 - if (sysEquipment.getIotModelId() != null){ - dataService.deviceModelMap.put(sysEquipment.getId().toString(),dataService.iotModelMap.get(sysEquipment.getIotModelId().toString())); + if (sysEquipment.getIotModelId() != null) { + dataService.deviceModelMap.put(sysEquipment.getId().toString(), dataService.iotModelMap.get(sysEquipment.getIotModelId().toString())); } //更新设备缓存 cacheService.getEquipmentCache().refreshDeviceCache(sysEquipment.getId()); @@ -147,14 +158,14 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { sysEquipment.setUpdatedBy(sysUserVo.getAccount()); sysEquipmentMapper.updateById(sysEquipment); - if (oldModelSysEquipInfo.getIotModelId() == null && sysEquipment.getIotModelId() != null){ - dataService.deviceModelMap.put(sysEquipment.getId().toString(),dataService.iotModelMap.get(sysEquipment.getIotModelId().toString())); + if (oldModelSysEquipInfo.getIotModelId() == null && sysEquipment.getIotModelId() != null) { + dataService.deviceModelMap.put(sysEquipment.getId().toString(), dataService.iotModelMap.get(sysEquipment.getIotModelId().toString())); } //更新设备缓存 cacheService.getEquipmentCache().refreshDeviceCache(sysEquipment.getId()); - if (tdEngineService.checkTableExist("e_"+sysEquipment.getId())){ - tdEngineService.updateTagDeviceCode("e_"+sysEquipment.getId(),sysEquipment.getCode()); - tdEngineService.updateTagDeviceName("e_"+sysEquipment.getId(),sysEquipment.getName()); + if (tdEngineService.checkTableExist("e_" + sysEquipment.getId())) { + tdEngineService.updateTagDeviceCode("e_" + sysEquipment.getId(), sysEquipment.getCode()); + tdEngineService.updateTagDeviceName("e_" + sysEquipment.getId(), sysEquipment.getName()); } SysEquipmentVo sysEquipmentVo = new SysEquipmentVo(); BeanCopyUtils.copy(sysEquipment, sysEquipmentVo); @@ -187,10 +198,10 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { public List queryAllSysEquipmentList(SysEquipmentDto sysEquipmentDto) { // 查询当前账号机构下的子机构和子设备 QueryWrapper queryWrapper = new QueryWrapper<>(); - if (sysEquipmentDto.getOrgId() !=null){ + if (sysEquipmentDto.getOrgId() != null) { queryWrapper.eq("org_id", sysEquipmentDto.getOrgId()); } - if (sysEquipmentDto.getParentEquipmentId() !=null){ + if (sysEquipmentDto.getParentEquipmentId() != null) { queryWrapper.eq("parent_equipment_id", sysEquipmentDto.getParentEquipmentId()); } queryWrapper.eq("object_type", sysEquipmentDto.getObjectType()); @@ -209,9 +220,10 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { /** * 设备台账导出Excel + * * @param sysEquipmentDto 查询参数 - * @param request HttpServletRequest - * @param response HttpServletResponse + * @param request HttpServletRequest + * @param response HttpServletResponse */ @Override public void exportSysEquipment(SysEquipmentDto sysEquipmentDto, HttpServletRequest request, HttpServletResponse response) { @@ -278,18 +290,18 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { List delSysEquipmentList = new ArrayList<>(); // 遍历 for (List row : list) { - if (ObjectUtil.isAllNotEmpty(row.get(4),row.get(1),row.get(5))){ - throw new ServerException("请检查必填参数:"+row); + if (ObjectUtil.isAllNotEmpty(row.get(4), row.get(1), row.get(5))) { + throw new ServerException("请检查必填参数:" + row); } - if (!Integer.valueOf(row.get(1).toString()).equals(EquipmentTypeIds.EQUIPMENT_TYPE_STATION_WTG) && !Integer.valueOf(row.get(1).toString()).equals(EquipmentTypeIds.EQUIPMENT_TYPE_WIND_FARM)){ - throw new ServerException("设备类型编码错误"+ row.get(1)); + if (!Integer.valueOf(row.get(1).toString()).equals(EquipmentTypeIds.EQUIPMENT_TYPE_STATION_WTG) && !Integer.valueOf(row.get(1).toString()).equals(EquipmentTypeIds.EQUIPMENT_TYPE_WIND_FARM)) { + throw new ServerException("设备类型编码错误" + row.get(1)); } SysEquipment field = new SysEquipment(); // 根据编码获取物模型id if (StringUtils.hasText(row.get(2).toString())) { Long iotModelId = sysIotModelMapper.queryIotModelIdByCode(row.get(2).toString()); - if (iotModelId == null){ - throw new ServerException("物模型编码错误,错误编码:"+ row.get(2).toString()); + if (iotModelId == null) { + throw new ServerException("物模型编码错误,错误编码:" + row.get(2).toString()); } field.setIotModelId(iotModelId); } @@ -323,7 +335,7 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { field.setParentEquipmentId(Long.valueOf(parentEquipmentId)); SysEquipmentVo info = sysEquipmentMapper.queryEquipmentInfoByCode(field.getCode()); - if(info == null) { + if (info == null) { if ("I".equals(row.get(0))) { //加入集合 // 遍历完一个添加一个 @@ -348,35 +360,35 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { } } sysEquipmentMapper.insertBatch(addSysEquipmentList); - for (SysEquipment item : addSysEquipmentList){ - if (item.getIotModelId() != null){ + for (SysEquipment item : addSysEquipmentList) { + if (item.getIotModelId() != null) { String modelCode = dataService.iotModelMap.get(item.getIotModelId().toString()); - dataService.deviceModelMap.put(item.getId().toString(),modelCode); + dataService.deviceModelMap.put(item.getId().toString(), modelCode); } //更新设备缓存 cacheService.getEquipmentCache().refreshDeviceCache(item.getId()); } if (CollectionUtils.isNotEmpty(updateSysEquipmentList)) { sysEquipmentMapper.updateBatchById(updateSysEquipmentList); - for (SysEquipment item : updateSysEquipmentList){ - if (item.getIotModelId() != null){ + for (SysEquipment item : updateSysEquipmentList) { + if (item.getIotModelId() != null) { String modelCode = dataService.iotModelMap.get(item.getIotModelId().toString()); - dataService.deviceModelMap.put(item.getId().toString(),modelCode); + dataService.deviceModelMap.put(item.getId().toString(), modelCode); } //更新设备缓存 cacheService.getEquipmentCache().refreshDeviceCache(item.getId()); //更新td表TAG - if (tdEngineService.checkTableExist("e_"+item.getId())){ - tdEngineService.updateTagDeviceCode("e_"+item.getId(),item.getCode()); - tdEngineService.updateTagDeviceName("e_"+item.getId(),item.getName()); + if (tdEngineService.checkTableExist("e_" + item.getId())) { + tdEngineService.updateTagDeviceCode("e_" + item.getId(), item.getCode()); + tdEngineService.updateTagDeviceName("e_" + item.getId(), item.getName()); } } } if (CollectionUtils.isNotEmpty(delSysEquipmentList)) { // 删除设备 sysEquipmentMapper.deleteBatchIds(delSysEquipmentList); - for (SysEquipment item : delSysEquipmentList){ + for (SysEquipment item : delSysEquipmentList) { dataService.deviceModelMap.remove(item.getId().toString()); //更新设备缓存 cacheService.getEquipmentCache().removeDeviceCache(item.getId()); @@ -387,7 +399,7 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { @Override public SysGenExtProps creatSysEquipmentExtProps(SysGenExtPropsDto sysGenExtPropsDto) { - if (sysGenExtPropsDto.getId() == null){ + if (sysGenExtPropsDto.getId() == null) { throw new ServiceException("设备id不能为空"); } SysGenExtProps sysEquipmentExtProps = new SysGenExtProps(); @@ -398,7 +410,7 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { @Override public SysGenExtProps updateSysEquipmentExtProps(SysGenExtPropsDto sysGenExtPropsDto) { - if (sysGenExtPropsDto.getId() == null){ + if (sysGenExtPropsDto.getId() == null) { throw new ServiceException("设备id不能为空"); } SysGenExtProps sysEquipmentExtProps = new SysGenExtProps(); @@ -418,22 +430,82 @@ public class SysEquipmentServiceImpl implements SysEquipmentService { } @Override - public SysEquipmentDocs upload(Long deviceId, String folderName, MultipartFile file) { - DeviceInfoCache deviceInfoCache = equipmentCache.getDeviceInfoCacheById(deviceId); - String parent = FileConstants.FILE_SEPARATOR +"风机图片"+ FileConstants.FILE_SEPARATOR + deviceInfoCache.getDeviceCode(); - String url = minioViewsServcie.upload(minioAutoProperties.getPublicBucket(), parent, folderName, file); - String fileName = url.substring(url.lastIndexOf("/")); + public List upload(Long deviceId, String component, String folderName, List fileList) throws Exception { + List result = new ArrayList<>(); + for (MultipartFile file : fileList) { + DeviceInfoCache deviceInfoCache = equipmentCache.getDeviceInfoCacheById(deviceId); + String parent = FileConstants.FILE_SEPARATOR + "WindTurbine" + FileConstants.FILE_SEPARATOR + deviceInfoCache.getDeviceCode() + FileConstants.FILE_SEPARATOR + "pic"; + File scale = null; + try { + String url = minioViewsServcie.upload(minioAutoProperties.getPublicBucket(), parent, folderName, file); + scale = scale(file); + String scaleFileName = scale.getName(); + String scaleName = scaleFileName.substring(scaleFileName.lastIndexOf("_") + 1); + String scaleParent = FileConstants.FILE_SEPARATOR + "WindTurbine" + FileConstants.FILE_SEPARATOR + deviceInfoCache.getDeviceCode() + FileConstants.FILE_SEPARATOR + "thumbnailPic" + FileConstants.FILE_SEPARATOR + scaleName; + minioViewsServcie.uploadTemFile(minioAutoProperties.getPublicBucket(), scale, scaleParent); + String fileName = url.substring(url.lastIndexOf("/") + 1); + SysEquipmentDocs sysEquipmentDocs = saveDocs(deviceId, component, fileName, url); + SysEquipmentDocs sysEquipmentDocsScale = saveDocs(deviceId, component, scaleName, scaleParent); + result.add(sysEquipmentDocs); + result.add(sysEquipmentDocsScale); + } catch (Exception e) { + log.error("图片上传失败{}", e); + } finally { + if (scale != null && scale.exists()){ + scale.delete(); + } + } + + } + return result; + } + + public File scale(MultipartFile file) throws IOException { + // 获取原始文件名和文件类型 + String originalFileName = file.getOriginalFilename(); + String originalContentType = file.getContentType(); + + if (originalFileName == null || originalFileName.isEmpty()) { + throw new IllegalArgumentException("文件名不能为空"); + } + + if (originalContentType == null || originalContentType.isEmpty()) { + throw new IllegalArgumentException("文件类型不能为空"); + } + // 创建临时文件,名称基于原始文件名 + File tempFile = File.createTempFile("thumbnail_", "_" + originalFileName); + InputStream inputStream = file.getInputStream(); + try { + File mulFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename()); + // 将MultipartFile写入临时文件 + file.transferTo(mulFile); + // 生成缩略图 + ImgUtil.scale(mulFile, tempFile, 700, 700, null); + } finally { + IoUtil.close(inputStream); + + } + return tempFile; + } + + public SysEquipmentDocs saveDocs(Long deviceId, String component,String fileName,String url){ SysEquipmentDocs sysEquipmentDocs = new SysEquipmentDocs(); sysEquipmentDocs.setDeviceId(deviceId); sysEquipmentDocs.setName(fileName); sysEquipmentDocs.setUrl(url); + sysEquipmentDocs.setComponent(component); sysEquipmentDocs.setUpdateTime(new Date()); - SysEquipmentDocs sysEquipmentDocsInfo = sysEquipmentDocsMapper.selectById(deviceId); - if (sysEquipmentDocsInfo == null){ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("url",url); + SysEquipmentDocs sysEquipmentDocsInfo = sysEquipmentDocsMapper.selectOne(queryWrapper); + if (sysEquipmentDocsInfo == null) { sysEquipmentDocsMapper.insert(sysEquipmentDocs); - }else { + } else { + sysEquipmentDocs.setId(sysEquipmentDocsInfo.getId()); sysEquipmentDocsMapper.updateById(sysEquipmentDocs); } return sysEquipmentDocs; } + + } diff --git a/das/src/main/java/com/das/modules/fdr/service/MinioViewsServcie.java b/das/src/main/java/com/das/modules/fdr/service/MinioViewsServcie.java index ac85e1b8..2bcf8a21 100644 --- a/das/src/main/java/com/das/modules/fdr/service/MinioViewsServcie.java +++ b/das/src/main/java/com/das/modules/fdr/service/MinioViewsServcie.java @@ -78,11 +78,11 @@ public class MinioViewsServcie { } - public String upload(String bucketName, String path, String folderName,MultipartFile file) { + public String upload(String bucketName, String path, String folderName, MultipartFile file) { String targetFile = null; try { - // 上传一个空对象来模拟文件夹 - if (!StringUtils.isBlank(folderName)){ + // 上传一个空对象来模拟文件夹 + if (!StringUtils.isBlank(folderName)) { targetFile = path + folderName + FileConstants.FILE_SEPARATOR; ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]); minioClient.putObject( @@ -91,9 +91,8 @@ public class MinioViewsServcie { .object(targetFile) .stream(bais, 0, -1) .build()); - } - else { - targetFile= path +"/" + file.getOriginalFilename(); + } else { + targetFile = path + "/" + file.getOriginalFilename(); uploadFile(bucketName, file, targetFile, "application/octet-stream"); } } catch (Exception e) { @@ -114,7 +113,7 @@ public class MinioViewsServcie { */ public void uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) throws Exception { InputStream inputStream = file.getInputStream(); - try{ + try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) @@ -122,12 +121,35 @@ public class MinioViewsServcie { .contentType(contentType) .stream(inputStream, inputStream.available(), -1) .build()); - }catch (Exception e){ + } catch (Exception e) { log.error("minio文件上传失败", e); } } + public void uploadTemFile(String bucketName, File file, String objectName) throws Exception { + + try (FileInputStream fileInputStream = new FileInputStream(file)) { + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .contentType("application/octet-stream") + .stream(fileInputStream, file.length(), -1) + .build() + ); + + + // 删除临时文件 + if (!file.delete()) { + System.err.println("临时文件删除失败:" + file.getAbsolutePath()); + } + } catch (Exception e) { + log.error("生成缩略图或上传到 MinIO 失败", e); + } + + } + //获取路径下的文件夹文件列表 public List getFileTree(String directoryName) { List fileNodes = new ArrayList<>(); @@ -137,7 +159,7 @@ public class MinioViewsServcie { if (StringUtils.isBlank(directoryName)) { build = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).recursive(true).build(); } else { - build = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).prefix(directoryName+"/").recursive(true).build(); + build = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).prefix(directoryName + "/").recursive(true).build(); } Iterable> results = minioClient.listObjects(build); for (Result result : results) { @@ -147,24 +169,23 @@ public class MinioViewsServcie { String size = FileUtil.readableFileSize(item.size()); String relativePath = null; String[] parts = null; - if (!StringUtils.isBlank(directoryName)){ + if (!StringUtils.isBlank(directoryName)) { relativePath = itemName.substring(directoryName.length()); parts = relativePath.split("/"); - } - else { + } else { parts = itemName.split("/"); } String lastModifyTime = null; - DateTimeFormatter dateFormat =DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); - if (!isDir){ + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); + if (!isDir) { ZonedDateTime zonedDateTime = item.lastModified(); lastModifyTime = zonedDateTime.format(dateFormat); } if (parts.length > 0) { String nodeName = parts[1]; int type = isDir ? 0 : 1; - itemName= isDir ? itemName.substring(0,itemName.lastIndexOf("/")) : itemName; - FileNode node = new FileNode(nodeName, type,size,lastModifyTime,"/"+itemName); + itemName = isDir ? itemName.substring(0, itemName.lastIndexOf("/")) : itemName; + FileNode node = new FileNode(nodeName, type, size, lastModifyTime, "/" + itemName); if (!fileNodes.contains(node)) { fileNodes.add(node); } @@ -178,15 +199,15 @@ public class MinioViewsServcie { public void readFileToStream(String path, OutputStream stream) { - try ( GetObjectResponse res = minioClient.getObject( - GetObjectArgs.builder().bucket(minioProperties.getBucket()).object(path).build())){ + try (GetObjectResponse res = minioClient.getObject( + GetObjectArgs.builder().bucket(minioProperties.getBucket()).object(path).build())) { res.transferTo(stream); } catch (Exception e) { log.error("minio读取文件失败", e); } } - public void download(String path, Path tempDir,HttpServletResponse httpServletResponse) { + public void download(String path, Path tempDir, HttpServletResponse httpServletResponse) { File tempFile = null; try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder() @@ -202,7 +223,7 @@ public class MinioViewsServcie { } finally { assert tempFile != null; - if (tempFile.exists()){ + if (tempFile.exists()) { tempFile.delete(); } } @@ -257,7 +278,7 @@ public class MinioViewsServcie { return total; } - public InputStream getFileStream(String url){ + public InputStream getFileStream(String url) { InputStream inputStream = null; try { diff --git a/das/src/main/resources/application.yml b/das/src/main/resources/application.yml index ed5945e9..2d60c655 100644 --- a/das/src/main/resources/application.yml +++ b/das/src/main/resources/application.yml @@ -109,6 +109,6 @@ tdengine: minio: url: http://192.168.109.187:9000 bucket: das - publicBucket: das-public + publicBucket: das-dock accessKey: das secretKey: zaq12WSX \ No newline at end of file