From 4b7772b38d782dbc80142fdb006c487dec76736c Mon Sep 17 00:00:00 2001 From: chenhao <852066789@qq.com> Date: Mon, 1 Sep 2025 16:46:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(file-upload):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=88=86=E7=89=87=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20USB=20=E6=8E=A7=E5=88=B6=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增分片上传相关类和接口,包括 ChunkedUploadService、ChunkedUploadResult等 - 实现分片上传、合并、取消上传等功能 - 添加 USB 开启和关闭控制接口 - 新增 ImageTool 相关实体和接口,用于管理工具文件 --- .../project/controller/DeviceController.java | 20 ++ .../controller/ImageToolController.java | 141 ++++++++++ .../project/entity/dao/ImageTool.java | 117 ++++++++ .../project/entity/enums/GrpcTypeEnum.java | 35 +++ .../project/entity/req/ImageCreateReq.java | 4 +- .../project/entity/req/ImageToolReq.java | 105 +++++++ .../entity/req/ImageVirtualMachinesReq.java | 15 + .../project/entity/res/ImageToolRes.java | 94 +++++++ .../project/mapper/ImageToolMapper.java | 18 ++ .../service/ChunkedUploadCompletion.java | 22 ++ .../project/service/ChunkedUploadResult.java | 60 ++++ .../project/service/ChunkedUploadService.java | 106 +++++++ .../project/service/ChunkedUploadStatus.java | 52 ++++ .../project/service/ClientOperateService.java | 4 + .../project/service/ImageToolService.java | 32 +++ .../impl/ChunkedUploadServiceImpl.java | 262 ++++++++++++++++++ .../impl/ClientOperateServiceImpl.java | 20 +- .../service/impl/ImageToolServiceImpl.java | 216 +++++++++++++++ .../impl/ImageVirtualMachinesServiceImpl.java | 31 ++- nex-be/src/main/resources/application.yml | 4 +- .../main/resources/mapper/ImageToolMaper.xml | 27 ++ 21 files changed, 1371 insertions(+), 14 deletions(-) create mode 100644 nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/entity/dao/ImageTool.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/entity/enums/GrpcTypeEnum.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/entity/req/ImageToolReq.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/entity/res/ImageToolRes.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/mapper/ImageToolMapper.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadCompletion.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadResult.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadService.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadStatus.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/impl/ChunkedUploadServiceImpl.java create mode 100644 nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java create mode 100644 nex-be/src/main/resources/mapper/ImageToolMaper.xml diff --git a/nex-be/src/main/java/com/unisinsight/project/controller/DeviceController.java b/nex-be/src/main/java/com/unisinsight/project/controller/DeviceController.java index 746cf19..2d045a8 100644 --- a/nex-be/src/main/java/com/unisinsight/project/controller/DeviceController.java +++ b/nex-be/src/main/java/com/unisinsight/project/controller/DeviceController.java @@ -106,5 +106,25 @@ public class DeviceController { return clientOperateService.terminalEnd(deviceReq); } + @ApiOperation(value = "usb开启") + @PostMapping("/terminal/usb/enabled") + public Result usbEnabled(@RequestBody DeviceReq deviceReq) { + if (Objects.isNull(deviceReq)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("终端usb开启请求参数为:{}", JSONUtil.toJsonStr(deviceReq)); + return clientOperateService.usbEnabled(deviceReq); + } + + @ApiOperation(value = "usb关闭") + @PostMapping("/terminal/usb/disabled") + public Result usbDisabled(@RequestBody DeviceReq deviceReq) { + if (Objects.isNull(deviceReq)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("终端usb关闭请求参数为:{}", JSONUtil.toJsonStr(deviceReq)); + return clientOperateService.usbDisabled(deviceReq); + } + } diff --git a/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java b/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java new file mode 100644 index 0000000..f1d38a4 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java @@ -0,0 +1,141 @@ +package com.unisinsight.project.controller; + + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.unisinsight.project.entity.dao.ImageTool; +import com.unisinsight.project.entity.req.ImageToolReq; +import com.unisinsight.project.entity.res.ImageToolRes; +import com.unisinsight.project.service.ChunkedUploadService; +import com.unisinsight.project.service.ImageToolService; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import javax.annotation.Resource; +import java.io.Serializable; +import java.util.List; + +import com.unisinsight.project.exception.Result; +import lombok.extern.slf4j.Slf4j; +import com.unisinsight.project.exception.BaseErrorCode; + +import java.util.Objects; + +import com.unisinsight.project.entity.req.DeleteIdReq; +import cn.hutool.json.JSONUtil; +import org.springframework.web.multipart.MultipartFile; + +/** + * (ImageTool)表控制层 + * + * @author ch + * @since 2025-08-29 14:58:39 + */ +@RestController +@Api(tags = "工具管理接口") +@Slf4j +@RequestMapping("/api/nex/v1/image_tool") +public class ImageToolController { + /** + * 服务对象 + */ + @Resource + private ImageToolService service; + @Autowired + private ChunkedUploadService chunkedUploadService; + + /** + * 分页查询所有数据 + * + * @param imageToolReq 查询实体 + * @return 所有数据 + */ + @PostMapping("/select/page") + @ApiOperation(value = "分页查询") + public Result selectPage(@RequestBody ImageToolReq imageToolReq) { + if (Objects.isNull(imageToolReq)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("分页查询请求参数为:{}", JSONUtil.toJsonStr(imageToolReq)); + return service.selectPage(imageToolReq); + } + + + @PostMapping("/add") + @ApiOperation(value = "上传ImageTool文件分片", notes = "上传单个ImageTool文件分片,当所有分片上传完成后自动合并文件并保存到数据库") + @ApiImplicitParams({ + @ApiImplicitParam(name = "chunk", value = "文件分片", required = true, dataType = "__File", paramType = "form"), + @ApiImplicitParam(name = "chunk_size", value = "文件分片大小", required = true, dataType = "int", paramType = "query"), + @ApiImplicitParam(name = "chunk_md5", value = "文件分片md5", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "file_id", value = "文件唯一标识符", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "file_type", value = "文件类型", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "shard_index", value = "当前分片编号(从1开始)", required = true, dataType = "int", paramType = "query"), + @ApiImplicitParam(name = "shard_total", value = "总分片数", required = true, dataType = "int", paramType = "query"), + @ApiImplicitParam(name = "file_name", value = "原始文件名", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "file_size", value = "文件总大小", required = true, dataType = "long", paramType = "query"), + @ApiImplicitParam(name = "file_version", value = "文件版本", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "description", value = "描述", dataType = "String", paramType = "query") + }) + public Result uploadChunk( + @RequestParam("chunk") MultipartFile chunk, + @RequestParam("chunk_size") int chunkSize, + @RequestParam("chunk_md5") String chunkMd5, + @RequestParam("file_id") String fileId, + @RequestParam("file_type") String fileType, + @RequestParam("shard_index") int chunkNumber, + @RequestParam("shard_total") int totalChunks, + @RequestParam("file_name") String fileName, + @RequestParam("file_size") long totalSize, + @RequestParam("file_version") String fileVersion, + @RequestParam(value = "description", required = false) String description + ) { + + return service.uploadChunk(chunk, chunkSize, chunkMd5, fileId, chunkNumber, totalChunks, + fileName, totalSize,description); + } + @PostMapping("/cancel/upload") + @ApiOperation(value = "取消上传ImageTool文件") + public Result cancelUpload(@RequestParam("file_id") String fileId) { + try { + chunkedUploadService.cancelUpload(fileId); + return Result.successResult(); + } catch (Exception e) { + log.error("取消上传失败,fileId:{},error:{}", fileId, e.getMessage(), e); + return Result.errorResultMessage(BaseErrorCode.HTTP_ERROR_CODE_500, "取消上传失败: " + e.getMessage()); + } + } + @ApiOperation(value = "修改") + @PostMapping("/update") + public Result updateUser(@RequestBody ImageToolReq imageToolReq) { + if (Objects.isNull(imageToolReq)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("修改请求参数为:{}", JSONUtil.toJsonStr(imageToolReq)); + return service.update(imageToolReq); + } + + @ApiOperation(value = "查询") + @PostMapping("/query") + public Result queryUser(@RequestBody ImageToolReq imageToolReq) { + if (Objects.isNull(imageToolReq)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("查询请求参数为:{}", JSONUtil.toJsonStr(imageToolReq)); + return service.query(imageToolReq); + } + + @ApiOperation(value = "删除") + @PostMapping("/delete") + public Result delete(@RequestBody DeleteIdReq deleteIdReq) { + if (Objects.isNull(deleteIdReq)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("删除请求参数为:{}", JSONUtil.toJsonStr(deleteIdReq)); + return service.delete(deleteIdReq); + } +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/entity/dao/ImageTool.java b/nex-be/src/main/java/com/unisinsight/project/entity/dao/ImageTool.java new file mode 100644 index 0000000..9b85792 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/entity/dao/ImageTool.java @@ -0,0 +1,117 @@ +package com.unisinsight.project.entity.dao; + + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + + +/** + * (ImageTool)表实体类 + * + * @author ch + * @since 2025-08-29 16:10:59 + */ +@TableName(value = "image_tool") +@Data +@ApiModel("") +public class ImageTool extends Model { + /** + * ${column.comment} + **/ + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty("${column.comment}") + private Integer id; + /** + * 文件名称 + **/ + + + @TableField(value = "file_name") + @ApiModelProperty("文件名称") + private String fileName; + /** + * 文件类型 + **/ + + + @TableField(value = "file_type") + @ApiModelProperty("文件类型") + private String fileType; + /** + * 文件大小 + **/ + + + @TableField(value = "file_size") + @ApiModelProperty("文件大小") + private Long fileSize; + /** + * 文件版本 + **/ + + + @TableField(value = "file_version") + @ApiModelProperty("文件版本") + private String fileVersion; + /** + * 创建时间 + **/ + + + @TableField(value = "create_time", fill = FieldFill.INSERT) + @ApiModelProperty("创建时间") + private String createTime; + /** + * 创建人 + **/ + + + @TableField(value = "create_user") + @ApiModelProperty("创建人") + private String createUser; + /** + * 更新时间 + **/ + + + @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) + @ApiModelProperty("更新时间") + private String updateTime; + /** + * 更新人 + **/ + + + @TableField(value = "update_user") + @ApiModelProperty("更新人") + private String updateUser; + /** + * 上传时间 + **/ + + + @TableField(value = "upload_time") + @ApiModelProperty("上传时间") + private String uploadTime; + /** + * 存储路径 + **/ + + + @TableField(value = "store_path") + @ApiModelProperty("存储路径") + private String storePath; + /** + * 备注 + **/ + + + @TableField(value = "description") + @ApiModelProperty("备注") + private String description; + +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/entity/enums/GrpcTypeEnum.java b/nex-be/src/main/java/com/unisinsight/project/entity/enums/GrpcTypeEnum.java new file mode 100644 index 0000000..842b2d1 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/entity/enums/GrpcTypeEnum.java @@ -0,0 +1,35 @@ +package com.unisinsight.project.entity.enums; + + +import lombok.Getter; + +/** + * @author : ch + * @version : 1.0 + * @ClassName : GrpcTypeEnum + * @Description : + * @DATE : Created in 9:20 2025/9/1 + *
       Copyright: Copyright(c) 2025     
+ *
       Company :   	紫光汇智信息技术有限公司		           
+ * Modification History: + * Date Author Version Discription + * -------------------------------------------------------------------------- + * 2025/09/01 ch 1.0 Why & What is modified: <修改原因描述> * + */ +@Getter +public enum GrpcTypeEnum { + SHUTDOWN("1", "关机"), + IMAGE_UPDATE("2", "镜像列表更新"), + USB_ENABLED("3", "USB开启"), + USB_DISABLED("4", "USB关闭"), + ; + + + private final String type; + private final String desc; + + GrpcTypeEnum(String type, String desc) { + this.type = type; + this.desc = desc; + } +} diff --git a/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageCreateReq.java b/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageCreateReq.java index 7f03482..28e9d64 100644 --- a/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageCreateReq.java +++ b/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageCreateReq.java @@ -37,8 +37,8 @@ public class ImageCreateReq { private Integer diskSize; @JsonProperty("disk_format") private String diskFormat; - @JsonProperty("storage_pool") - private String storagePool; + @JsonProperty("storage_pool_name") + private String storagePoolName; @JsonProperty("network_name") private String networkName; @JsonProperty("iso_path") diff --git a/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageToolReq.java b/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageToolReq.java new file mode 100644 index 0000000..9f89f39 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageToolReq.java @@ -0,0 +1,105 @@ +package com.unisinsight.project.entity.req; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * (ImageTool)表实体类 + * + * @author ch + * @since 2025-08-29 16:10:59 + */ +@Data +@ApiModel("") +public class ImageToolReq { + /** + * ${column.comment} + **/ + @JsonProperty("id") + @ApiModelProperty("${column.comment}") + private Integer id; + /** + * 文件名称 + **/ + @JsonProperty("file_name") + @ApiModelProperty("文件名称") + private String fileName; + /** + * 文件类型 + **/ + @JsonProperty("file_type") + @ApiModelProperty("文件类型") + private String fileType; + /** + * 文件大小 + **/ + @JsonProperty("file_size") + @ApiModelProperty("文件大小") + private Long fileSize; + /** + * 文件版本 + **/ + @JsonProperty("file_version") + @ApiModelProperty("文件版本") + private String fileVersion; + /** + * 创建时间 + **/ + @JsonProperty("create_time") + @ApiModelProperty("创建时间") + private String createTime; + /** + * 创建人 + **/ + @JsonProperty("create_user") + @ApiModelProperty("创建人") + private String createUser; + /** + * 更新时间 + **/ + @JsonProperty("update_time") + @ApiModelProperty("更新时间") + private String updateTime; + /** + * 更新人 + **/ + @JsonProperty("update_user") + @ApiModelProperty("更新人") + private String updateUser; + /** + * 上传时间 + **/ + @JsonProperty("upload_time") + @ApiModelProperty("上传时间") + private String uploadTime; + /** + * 存储路径 + **/ + @JsonProperty("store_path") + @ApiModelProperty("存储路径") + private String storePath; + /** + * 备注 + **/ + @JsonProperty("description") + @ApiModelProperty("备注") + private String description; + /** + * 查询页 + */ + @ApiModelProperty(value = "查询页", notes = "分页查询时再传") + @JsonProperty("page_num") + private Integer pageNum; + + + /** + * 每页数量 + */ + @ApiModelProperty(value = "每页数量", notes = "分页查询时再传") + @JsonProperty("page_size") + private Integer pageSize; +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageVirtualMachinesReq.java b/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageVirtualMachinesReq.java index 3afa0e9..8b06e70 100644 --- a/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageVirtualMachinesReq.java +++ b/nex-be/src/main/java/com/unisinsight/project/entity/req/ImageVirtualMachinesReq.java @@ -51,6 +51,21 @@ public class ImageVirtualMachinesReq { @JsonProperty("storage_path") @ApiModelProperty("镜像存储路径") private String storagePath; + + @JsonProperty("storage_pool_name") + @ApiModelProperty("存储卷名称") + private String storagePoolName; + + + /** + * 驱动名称 + **/ + @JsonProperty("image_tool_name") + @ApiModelProperty("驱动名称") + private String imageToolName; + + + /** * 创建时间 **/ diff --git a/nex-be/src/main/java/com/unisinsight/project/entity/res/ImageToolRes.java b/nex-be/src/main/java/com/unisinsight/project/entity/res/ImageToolRes.java new file mode 100644 index 0000000..5c748d9 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/entity/res/ImageToolRes.java @@ -0,0 +1,94 @@ +package com.unisinsight.project.entity.res; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * (ImageTool)表实体类 + * + * @author ch + * @since 2025-08-29 16:10:59 + */ +@Data +@ApiModel("") +public class ImageToolRes implements Serializable { + /** + * ${column.comment} + **/ + @JsonProperty("id") + @ApiModelProperty("${column.comment}") + private Integer id; + /** + * 文件名称 + **/ + @JsonProperty("file_name") + @ApiModelProperty("文件名称") + private String fileName; + /** + * 文件类型 + **/ + @JsonProperty("file_type") + @ApiModelProperty("文件类型") + private String fileType; + /** + * 文件大小 + **/ + @JsonProperty("file_size") + @ApiModelProperty("文件大小") + private Long fileSize; + /** + * 文件版本 + **/ + @JsonProperty("file_version") + @ApiModelProperty("文件版本") + private String fileVersion; + /** + * 创建时间 + **/ + @JsonProperty("create_time") + @ApiModelProperty("创建时间") + private String createTime; + /** + * 创建人 + **/ + @JsonProperty("create_user") + @ApiModelProperty("创建人") + private String createUser; + /** + * 更新时间 + **/ + @JsonProperty("update_time") + @ApiModelProperty("更新时间") + private String updateTime; + /** + * 更新人 + **/ + @JsonProperty("update_user") + @ApiModelProperty("更新人") + private String updateUser; + /** + * 上传时间 + **/ + @JsonProperty("upload_time") + @ApiModelProperty("上传时间") + private String uploadTime; + /** + * 存储路径 + **/ + @JsonProperty("store_path") + @ApiModelProperty("存储路径") + private String storePath; + /** + * 备注 + **/ + @JsonProperty("description") + @ApiModelProperty("备注") + private String description; + +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/mapper/ImageToolMapper.java b/nex-be/src/main/java/com/unisinsight/project/mapper/ImageToolMapper.java new file mode 100644 index 0000000..a17bca7 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/mapper/ImageToolMapper.java @@ -0,0 +1,18 @@ +package com.unisinsight.project.mapper; + +import java.util.List; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import com.unisinsight.project.entity.dao.ImageTool; + +/** + * (ImageTool)表数据库访问层 + * + * @author ch + * @since 2025-08-29 15:24:09 + */ +public interface ImageToolMapper extends BaseMapper { + +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadCompletion.java b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadCompletion.java new file mode 100644 index 0000000..c1e8076 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadCompletion.java @@ -0,0 +1,22 @@ +package com.unisinsight.project.service; + +import lombok.Data; + +/** + * 分片上传完成事件数据 + */ +@Data +public class ChunkedUploadCompletion { + private String fileId; + private String fileName; + private String filePath; + private long fileSize; + private String md5; // 文件整体MD5,如果需要的话 + + public ChunkedUploadCompletion(String fileId, String fileName, String filePath, long fileSize) { + this.fileId = fileId; + this.fileName = fileName; + this.filePath = filePath; + this.fileSize = fileSize; + } +} \ No newline at end of file diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadResult.java b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadResult.java new file mode 100644 index 0000000..a87ae80 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadResult.java @@ -0,0 +1,60 @@ +package com.unisinsight.project.service; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +/** + * 分片上传结果类 + */ +@Data +public class ChunkedUploadResult { + private boolean success; + private String status; + private String message; + private String filePath; + private Integer uploadedChunks; + private Integer totalChunks; + private Map additionalData = new HashMap<>(); + + public static ChunkedUploadResult success(String status, String message) { + ChunkedUploadResult result = new ChunkedUploadResult(); + result.setSuccess(true); + result.setStatus(status); + result.setMessage(message); + return result; + } + + public static ChunkedUploadResult success(String status, String message, String filePath) { + ChunkedUploadResult result = new ChunkedUploadResult(); + result.setSuccess(true); + result.setStatus(status); + result.setMessage(message); + result.setFilePath(filePath); + return result; + } + + public static ChunkedUploadResult uploading(String message, int uploadedChunks, int totalChunks) { + ChunkedUploadResult result = new ChunkedUploadResult(); + result.setSuccess(true); + result.setStatus("uploading"); + result.setMessage(message); + result.setUploadedChunks(uploadedChunks); + result.setTotalChunks(totalChunks); + return result; + } + + public static ChunkedUploadResult error(String message) { + ChunkedUploadResult result = new ChunkedUploadResult(); + result.setSuccess(false); + result.setStatus("error"); + result.setMessage(message); + return result; + } + + public ChunkedUploadResult addData(String key, Object value) { + this.additionalData.put(key, value); + return this; + } +} \ No newline at end of file diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadService.java b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadService.java new file mode 100644 index 0000000..8f41287 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadService.java @@ -0,0 +1,106 @@ +package com.unisinsight.project.service; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.function.Consumer; + +/** + * 通用分片上传服务接口 + */ +public interface ChunkedUploadService { + + /** + * 上传文件分片 + * + * @param chunk 分片文件 + * @param chunkSize 分片大小 + * @param chunkMd5 分片MD5 + * @param fileId 文件唯一标识符 + * @param chunkNumber 当前分片编号(从1开始) + * @param totalChunks 总分片数 + * @param fileName 原始文件名 + * @param totalSize 文件总大小 + * @param onComplete 上传完成后的回调函数 + * @return 上传结果 + * @throws IOException IO异常 + */ + ChunkedUploadResult uploadChunk( + MultipartFile chunk, + int chunkSize, + String chunkMd5, + String fileId, + int chunkNumber, + int totalChunks, + String fileName, + long totalSize, + String fileSavePath, + Consumer onComplete + ) throws IOException; + + /** + * 上传文件分片(无回调) + * + * @param chunk 分片文件 + * @param chunkSize 分片大小 + * @param chunkMd5 分片MD5 + * @param fileId 文件唯一标识符 + * @param chunkNumber 当前分片编号(从1开始) + * @param totalChunks 总分片数 + * @param fileName 原始文件名 + * @param totalSize 文件总大小 + * @return 上传结果 + * @throws IOException IO异常 + */ + ChunkedUploadResult uploadChunk( + MultipartFile chunk, + int chunkSize, + String chunkMd5, + String fileId, + int chunkNumber, + int totalChunks, + String fileName, + long totalSize + ) throws IOException; + + /** + * 取消上传文件 + * + * @param fileId 文件唯一标识符 + */ + void cancelUpload(String fileId); + + /** + * 查询文件上传状态 + * + * @param fileId 文件唯一标识符 + * @return 上传状态信息 + */ + ChunkedUploadStatus getUploadStatus(String fileId); + + /** + * 合并所有分片文件 + * + * @param fileId 文件唯一标识符 + * @param outputPath 合并后的文件路径 + * @param totalChunks 总分片数 + * @throws IOException IO异常 + */ + void mergeChunks(String fileId, String outputPath, int totalChunks) throws IOException; + + /** + * 清理临时分片文件 + * + * @param fileId 文件唯一标识符 + * @throws IOException IO异常 + */ + void cleanupTempFiles(String fileId) throws IOException; + + /** + * 根据文件名删除已上传的文件 + * + * @param fileName 文件名 + * @throws IOException IO异常 + */ + void cleanUploadFile(String fileName) throws IOException; +} \ No newline at end of file diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadStatus.java b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadStatus.java new file mode 100644 index 0000000..1fa10d7 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/ChunkedUploadStatus.java @@ -0,0 +1,52 @@ +package com.unisinsight.project.service; + +import lombok.Data; + +/** + * 分片上传状态类 + */ +@Data +public class ChunkedUploadStatus { + private boolean success; + private String status; + private String message; + private String filePath; + private Integer uploadedChunks; + private Integer totalChunks; + private Double progress; + + public static ChunkedUploadStatus completed(String filePath) { + ChunkedUploadStatus status = new ChunkedUploadStatus(); + status.setSuccess(true); + status.setStatus("completed"); + status.setMessage("文件上传已完成"); + status.setFilePath(filePath); + return status; + } + + public static ChunkedUploadStatus uploading(int uploadedChunks, int totalChunks) { + ChunkedUploadStatus status = new ChunkedUploadStatus(); + status.setSuccess(true); + status.setStatus("uploading"); + status.setUploadedChunks(uploadedChunks); + status.setTotalChunks(totalChunks); + status.setProgress((double) uploadedChunks / totalChunks); + return status; + } + + public static ChunkedUploadStatus notFound() { + ChunkedUploadStatus status = new ChunkedUploadStatus(); + status.setSuccess(true); + status.setStatus("not_found"); + status.setMessage("文件上传信息不存在"); + return status; + } + + public static ChunkedUploadStatus error(String message) { + ChunkedUploadStatus status = new ChunkedUploadStatus(); + status.setSuccess(false); + status.setStatus("error"); + status.setMessage(message); + return status; + } +} \ No newline at end of file diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ClientOperateService.java b/nex-be/src/main/java/com/unisinsight/project/service/ClientOperateService.java index 6bcf794..aead7b9 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/ClientOperateService.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/ClientOperateService.java @@ -22,4 +22,8 @@ public interface ClientOperateService { Result terminalStart(DeviceReq deviceReq); Result terminalEnd(DeviceReq deviceReq); + + Result usbEnabled(DeviceReq deviceReq); + + Result usbDisabled(DeviceReq deviceReq); } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java b/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java new file mode 100644 index 0000000..3285d37 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java @@ -0,0 +1,32 @@ +package com.unisinsight.project.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.unisinsight.project.entity.dao.ImageTool; +import com.unisinsight.project.entity.req.DeleteIdReq; +import com.unisinsight.project.entity.req.ImageToolReq; +import com.unisinsight.project.entity.res.ImageToolRes; +import com.unisinsight.project.entity.res.PageResult; +import com.unisinsight.project.exception.Result; +import org.springframework.web.multipart.MultipartFile; + +/** + * (ImageTool)表服务接口 + * + * @author ch + * @since 2025-08-29 14:58:39 + */ +public interface ImageToolService extends IService { + Result> selectPage(ImageToolReq imageToolReq); + + Result insert(ImageToolReq imageToolReq); + + Result update(ImageToolReq imageToolReq); + + Result query(ImageToolReq imageToolReq); + + Result delete(DeleteIdReq deleteIdReq); + + Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, int totalChunks, String fileName, long totalSize, String description); +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ChunkedUploadServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ChunkedUploadServiceImpl.java new file mode 100644 index 0000000..9492035 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ChunkedUploadServiceImpl.java @@ -0,0 +1,262 @@ +package com.unisinsight.project.service.impl; + +import com.unisinsight.project.service.*; +import com.unisinsight.project.util.DigestUtil; +import com.unisinsight.project.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +/** + * 通用分片上传服务实现类 + */ +@Service +@Slf4j +public class ChunkedUploadServiceImpl implements ChunkedUploadService { + + // 临时目录,用于存储上传的分片 + @Value("${file.upload.temp-dir:${java.io.tmpdir}/chunked-uploads}") + private String tempDir; + + // 最终文件存储目录 + @Value("${file.upload.dir:${user.home}/uploads}") + private String uploadDir; + + // 存储每个文件的分片信息 + private final ConcurrentMap fileUploadMap = new ConcurrentHashMap<>(); + + @Override + public ChunkedUploadResult uploadChunk( + MultipartFile chunk, + int chunkSize, + String chunkMd5, + String fileId, + int chunkNumber, + int totalChunks, + String fileName, + long totalSize, + String fileSavePath, + Consumer onComplete + ) throws IOException { + try { + // MD5校验 + String md5 = DigestUtil.encryptMd5(chunk.getBytes()); + if (!chunkMd5.equals(md5)) { + log.info("分片文件md5校验失败,chunkMd5:{},md5:{}", chunkMd5, md5); + throw new RuntimeException("分片文件md5校验失败"); + } + + // 创建临时目录 + Path fileTempDir = Paths.get(tempDir, fileId); + if (!Files.exists(fileTempDir)) { + Files.createDirectories(fileTempDir); + } + + // 保存分片文件 + String chunkFileName = String.format("%05d.part", chunkNumber); + log.info("保存分片文件: {}", chunkFileName); + Path chunkFilePath = fileTempDir.resolve(chunkFileName); + chunk.transferTo(chunkFilePath); + + // 更新文件上传信息 + FileUploadInfo uploadInfo = fileUploadMap.computeIfAbsent(fileId, + id -> new FileUploadInfo(id, fileName, totalChunks, totalSize)); + uploadInfo.addUploadedChunk(chunkNumber); + + // 检查是否所有分片都已上传 + if (uploadInfo.isUploadComplete()) { + // 合并文件 + Path finalDir = Paths.get(StringUtil.isNotEmpty(fileSavePath) ? fileSavePath : uploadDir); + if (!Files.exists(finalDir)) { + Files.createDirectories(finalDir); + } + Path finalFilePath = finalDir.resolve(fileName); + log.info("合并所有分片文件: {}", finalFilePath); + mergeChunks(fileId, finalFilePath.toString(), totalChunks); + + // 清理临时文件 + log.info("清理临时文件: {}", fileId); + cleanupTempFiles(fileId); + + // 从上传映射中移除 + fileUploadMap.remove(fileId); + + // 调用完成回调 + if (onComplete != null) { + ChunkedUploadCompletion completion = new ChunkedUploadCompletion( + fileId, fileName, finalFilePath.toString(), totalSize); + onComplete.accept(completion); + } + + return ChunkedUploadResult.success("completed", "文件上传并合并完成", finalFilePath.toString()); + } else { + return ChunkedUploadResult.uploading("分片上传成功", uploadInfo.getUploadedChunks().size(), totalChunks); + } + } catch (Exception e) { + log.error("上传失败,fileId:{},error:{}", fileId, e.getMessage(), e); + try { + cleanupTempFiles(fileId); + cleanUploadFile(fileName); + } catch (IOException ex) { + log.error("清理临时文件失败,fileId:{},error:{}", fileId, ex.getMessage(), ex); + } + throw e; + } + } + + @Override + public ChunkedUploadResult uploadChunk( + MultipartFile chunk, + int chunkSize, + String chunkMd5, + String fileId, + int chunkNumber, + int totalChunks, + String fileName, + long totalSize + ) throws IOException { + return uploadChunk(chunk, chunkSize, chunkMd5, fileId, chunkNumber, totalChunks, fileName, totalSize, null, null); + } + + @Override + public void cancelUpload(String fileId) { + if (fileId == null || fileId.isEmpty()) { + return; + } + log.info("取消上传,清理临时文件: {}", fileId); + try { + // 从上传映射中移除 + fileUploadMap.remove(fileId); + cleanupTempFiles(fileId); + } catch (IOException ex) { + log.error("清理临时文件失败,fileId:{},error:{}", fileId, ex.getMessage(), ex); + } + } + + @Override + public ChunkedUploadStatus getUploadStatus(String fileId) { + FileUploadInfo uploadInfo = fileUploadMap.get(fileId); + if (uploadInfo == null) { + // 检查文件是否已经完成上传并合并 + try { + Path finalFilePath = Paths.get(uploadDir, fileId); + if (Files.exists(finalFilePath)) { + return ChunkedUploadStatus.completed(finalFilePath.toString()); + } else { + return ChunkedUploadStatus.notFound(); + } + } catch (Exception e) { + return ChunkedUploadStatus.error("查询状态失败: " + e.getMessage()); + } + } else { + return ChunkedUploadStatus.uploading(uploadInfo.getUploadedChunks().size(), uploadInfo.getTotalChunks()); + } + } + + @Override + public void mergeChunks(String fileId, String outputPath, int totalChunks) throws IOException { + Path finalPath = Paths.get(outputPath); + Path tempOutputPath = Paths.get(outputPath + ".tmp"); + + try { + // 先写入临时文件 + try (OutputStream outputStream = Files.newOutputStream(tempOutputPath)) { + Path fileTempDir = Paths.get(tempDir, fileId); + + // 按顺序合并分片 + for (int i = 1; i <= totalChunks; i++) { + String chunkFileName = String.format("%05d.part", i); + Path chunkPath = fileTempDir.resolve(chunkFileName); + + if (!Files.exists(chunkPath)) { + throw new IOException("缺少分片文件: " + chunkFileName); + } + + // 将分片内容追加到输出文件 + Files.copy(chunkPath, outputStream); + } + } + + // 原子性地替换目标文件 + Files.move(tempOutputPath, finalPath, StandardCopyOption.REPLACE_EXISTING); + } finally { + // 确保临时文件被删除 + if (Files.exists(tempOutputPath)) { + Files.delete(tempOutputPath); + } + } + } + + @Override + public void cleanupTempFiles(String fileId) throws IOException { + Path fileTempDir = Paths.get(tempDir, fileId); + if (Files.exists(fileTempDir)) { + // 递归删除临时目录及其内容 + Files.walk(fileTempDir) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Override + public void cleanUploadFile(String fileName) throws IOException { + Path filePath = Paths.get(uploadDir, fileName); + if (Files.exists(filePath)) { + // 删除文件 + Files.delete(filePath); + log.info("已删除文件: {}", filePath); + } else { + log.warn("文件不存在,无需删除: {}", filePath); + } + } + + /** + * 文件上传信息类 + */ + private static class FileUploadInfo { + private final String fileId; + private final String fileName; + private final int totalChunks; + private final long totalSize; + private final Set uploadedChunks; + + public FileUploadInfo(String fileId, String fileName, int totalChunks, long totalSize) { + this.fileId = fileId; + this.fileName = fileName; + this.totalChunks = totalChunks; + this.totalSize = totalSize; + this.uploadedChunks = ConcurrentHashMap.newKeySet(); + } + + public void addUploadedChunk(int chunkNumber) { + uploadedChunks.add(chunkNumber); + } + + public boolean isUploadComplete() { + return uploadedChunks.size() == totalChunks; + } + + public Set getUploadedChunks() { + return uploadedChunks; + } + + public int getTotalChunks() { + return totalChunks; + } + } +} \ No newline at end of file diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientOperateServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientOperateServiceImpl.java index b4defb5..8cfc048 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientOperateServiceImpl.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientOperateServiceImpl.java @@ -1,6 +1,7 @@ package com.unisinsight.project.service.impl; +import com.unisinsight.project.entity.enums.GrpcTypeEnum; import com.unisinsight.project.entity.req.DeviceReq; import com.unisinsight.project.exception.Result; import com.unisinsight.project.grpc.generate.NotificationMessage; @@ -35,7 +36,22 @@ public class ClientOperateServiceImpl implements ClientOperateService { @Override public Result terminalEnd(DeviceReq deviceReq) { - //todo 待客户端确认消息内容后完善 - return notificationService.sendNotification(deviceReq.getDeviceId(), NotificationMessage.newBuilder().setContent("终端关机").build()); + return notificationService.sendNotification(deviceReq.getDeviceId(), NotificationMessage.newBuilder() + .setType(GrpcTypeEnum.SHUTDOWN.getType()) + .setContent(GrpcTypeEnum.SHUTDOWN.getDesc()).build()); + } + + @Override + public Result usbEnabled(DeviceReq deviceReq) { + return notificationService.sendNotification(deviceReq.getDeviceId(), NotificationMessage.newBuilder() + .setType(GrpcTypeEnum.USB_ENABLED.getType()) + .setContent(GrpcTypeEnum.USB_ENABLED.getDesc()).build()); + } + + @Override + public Result usbDisabled(DeviceReq deviceReq) { + return notificationService.sendNotification(deviceReq.getDeviceId(), NotificationMessage.newBuilder() + .setType(GrpcTypeEnum.USB_DISABLED.getType()) + .setContent(GrpcTypeEnum.USB_DISABLED.getDesc()).build()); } } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java new file mode 100644 index 0000000..6470b17 --- /dev/null +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java @@ -0,0 +1,216 @@ +package com.unisinsight.project.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.unisinsight.project.entity.dao.ImageTool; +import com.unisinsight.project.entity.req.DeleteIdReq; +import com.unisinsight.project.entity.req.ImageToolReq; +import com.unisinsight.project.entity.res.ImageToolRes; +import com.unisinsight.project.entity.res.PageResult; +import com.unisinsight.project.exception.BaseErrorCode; +import com.unisinsight.project.exception.Result; +import com.unisinsight.project.mapper.ImageToolMapper; +import com.unisinsight.project.service.ChunkedUploadCompletion; +import com.unisinsight.project.service.ChunkedUploadResult; +import com.unisinsight.project.service.ChunkedUploadService; +import com.unisinsight.project.service.ImageToolService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +@Slf4j +/** + * (ImageTool)表服务实现类 + * + * @author ch + * @since 2025-08-29 14:58:39 + */ +@Service("imageToolService") +public class ImageToolServiceImpl extends ServiceImpl implements ImageToolService { + @Resource + private ImageToolMapper mapper; + + @Resource + private ChunkedUploadService chunkedUploadService; + + @Value("${file.upload.tool.dir:${user.home}/uploads/tool}") + private String uploadDir; + + @Override + public Result> selectPage(ImageToolReq imageToolReq) { + Page page = new Page<>(imageToolReq.getPageNum(), imageToolReq.getPageSize()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByAsc(ImageTool::getId); + Page imageToolPage = mapper.selectPage(page, queryWrapper); + log.info("分页查询返回:{}", JSONUtil.toJsonStr(imageToolPage)); + if (CollectionUtil.isEmpty(imageToolPage.getRecords())) { + log.info("分页查询返回为空"); + return Result.successResult(); + } else { + PageResult convert = PageResult.convertIPage(imageToolPage, ImageToolRes.class); + List data = convert.getData(); + convert.setData(data); + return Result.successResult(convert); + } + } + + @Override + public Result insert(ImageToolReq imageToolReq) { + ImageTool imageTool = BeanUtil.copyProperties(imageToolReq, ImageTool.class); + imageTool.setCreateUser("admin"); + imageTool.setUpdateUser("admin"); + int insert = mapper.insert(imageTool); + log.info("新增insert:{}", insert); + + if (insert == 1) { + return Result.successResult(); + } else { + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500); + } + } + + @Override + public Result update(ImageToolReq imageToolReq) { + ImageTool imageTool = BeanUtil.copyProperties(imageToolReq, ImageTool.class); + imageTool.setUpdateUser("admin"); + int updated = mapper.updateById(imageTool); + log.info("修改updated:{}", updated); + if (updated == 1) { + return Result.successResult(); + } else { + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500); + } + } + + @Override + public Result query(ImageToolReq imageToolReq) { + ImageTool imageTool = mapper.selectById(imageToolReq.getId()); + + if (ObjectUtils.isEmpty(imageTool)) { + log.info("查询返回为空"); + return Result.successResult(); + } + ImageToolRes res = BeanUtil.copyProperties(imageTool, ImageToolRes.class); + log.info("查询返回:{}", JSONUtil.toJsonStr(res)); + return Result.successResult(res); + } + + @Override + public Result delete(DeleteIdReq deleteIdReq) { + int deleted = mapper.deleteById(deleteIdReq.getId()); + log.info("删除insert:{}", deleted); + if (deleted == 1) { + return Result.successResult(); + } else { + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500); + } + } + + @Override + public Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, int totalChunks, String fileName, long totalSize, String description) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ImageTool::getFileName, fileName); + ImageTool exists = mapper.selectOne(queryWrapper); + if (exists!=null){ + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500,"文件名重复"); + } + + Map response = new HashMap<>(); + try { + ChunkedUploadResult result = chunkedUploadService.uploadChunk( + chunk, chunkSize, chunkMd5, fileId, chunkNumber, totalChunks, + fileName, totalSize, uploadDir, completion -> { + try { + // 文件上传完成,保存到数据库 + ImageTool imageTool = new ImageTool(); + imageTool.setFileName(completion.getFileName()); + imageTool.setFileType(getFileType(completion.getFileName())); + imageTool.setFileSize(completion.getFileSize()); +// imageTool.setFileVersion(fileVersion); + imageTool.setStorePath(completion.getFilePath()); + imageTool.setUploadTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + imageTool.setCreateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + imageTool.setCreateUser("admin"); + imageTool.setUpdateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + imageTool.setUpdateUser("admin"); + if (description != null && !description.isEmpty()) { + imageTool.setDescription(description); + } + + boolean save = this.save(imageTool); + log.info("ImageTool新增结果: {}", save); + + if (!save) { + // 如果保存数据库失败,删除已上传的文件 + try { + chunkedUploadService.cleanUploadFile(completion.getFileName()); + } catch (Exception e) { + log.error("清理上传文件失败,fileName:{},error:{}", completion.getFileName(), e.getMessage(), e); + } + } + } catch (Exception e) { + log.error("保存ImageTool失败,fileId:{},error:{}", completion.getFileId(), e.getMessage(), e); + } + } + ); + if (result.isSuccess()) { + if ("completed".equals(result.getStatus())) { + response.put("status", "completed"); + response.put("message", "文件上传并合并完成"); + response.put("filePath", result.getFilePath()); + } else { + response.put("status", "uploading"); + response.put("message", "分片上传成功"); + response.put("uploadedChunks", result.getUploadedChunks()); + response.put("totalChunks", totalChunks); + } + } + + } catch (IOException e) { + response.put("success", false); + response.put("status", "error"); + response.put("message", "上传失败"); + log.info("上次失败清理临时文件: {},error:{}", fileId, e.getMessage(), e); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500); + } + + response.put("success", true); + return Result.successResult(response); + } + + + + /** + * 根据文件名获取文件类型 + * + * @param fileName 文件名 + * @return 文件类型 + */ + private String getFileType(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return ""; + } + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) { + return fileName.substring(lastDotIndex + 1).toLowerCase(); + } + return ""; + } +} + diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageVirtualMachinesServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageVirtualMachinesServiceImpl.java index 5cb67aa..00170ef 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageVirtualMachinesServiceImpl.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageVirtualMachinesServiceImpl.java @@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.unisinsight.project.entity.dao.Image; +import com.unisinsight.project.entity.dao.ImageTool; import com.unisinsight.project.entity.dao.ImageVirtualMachines; import com.unisinsight.project.entity.req.DeleteIdReq; import com.unisinsight.project.entity.req.ImageCreateReq; @@ -22,6 +23,7 @@ import com.unisinsight.project.exception.Result; import com.unisinsight.project.mapper.ImageVirtualMachinesMapper; import com.unisinsight.project.properties.ImageConfigProperties; import com.unisinsight.project.service.ImageService; +import com.unisinsight.project.service.ImageToolService; import com.unisinsight.project.service.ImageVirtualMachinesService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -54,7 +56,8 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl insert(ImageVirtualMachinesReq imageVirtualMachinesReq) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(ImageVirtualMachines::getImageName, imageVirtualMachinesReq.getImageName()); + ImageVirtualMachines selectOne = machinesMapper.selectOne(queryWrapper); + if (ObjectUtils.isNotEmpty(selectOne)) { + return Result.errorResultMessage(BaseErrorCode.HTTP_ERROR_CODE_500, "名称重复"); + } + + // 调用镜像生成服务 LambdaQueryWrapper imageLambdaQueryWrapper = new LambdaQueryWrapper<>(); imageLambdaQueryWrapper.in(Image::getId, imageVirtualMachinesReq.getImageSystemId()); Image systemImage = imageService.getOne(imageLambdaQueryWrapper); + //查询驱动信息 + LambdaQueryWrapper imageToolLambdaQueryWrapper = new LambdaQueryWrapper<>(); + imageToolLambdaQueryWrapper.eq(ImageTool::getFileName, imageVirtualMachinesReq.getImageName()); + ImageTool imageTool = imageToolService.getOne(imageToolLambdaQueryWrapper); + + + ImageCreateReq createReq = ImageCreateReq.builder() .name(imageVirtualMachinesReq.getImageName()) .osType(imageVirtualMachinesReq.getOsVersion()) @@ -137,7 +155,7 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda().eq(ImageVirtualMachines::getImageName, imageVirtualMachines.getImageName()); - ImageVirtualMachines selectOne = machinesMapper.selectOne(queryWrapper); - if (ObjectUtils.isNotEmpty(selectOne)) { - return new Result<>("200", "名称重复"); - } + imageVirtualMachines.setCreateUser("admin"); imageVirtualMachines.setUpdateUser("admin"); int insert = machinesMapper.insert(imageVirtualMachines); @@ -167,7 +180,7 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl + + + + + + + + + + + + + + + + + + + + + id, file_name, file_type, file_size, file_version, create_time, create_user, update_time, update_user, upload_time, store_path, description + + + + + From 0292dcb065f6152a870b7d8155558d962a343bad Mon Sep 17 00:00:00 2001 From: chenhao <852066789@qq.com> Date: Tue, 2 Sep 2025 09:56:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(virtual-machines):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=99=9A=E6=8B=9F=E6=9C=BA=E6=93=8D=E4=BD=9C=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增了虚拟机启动、关闭、强制关闭、重启等操作的接口和实现 - 增加了克隆虚拟机到桌面镜像的功能 - 更新了虚拟机删除逻辑,支持删除存储的镜像文件 - 重构了部分代码,优化了虚拟机相关数据的处理 --- .../controller/ImageToolController.java | 50 ++++- .../ImageVirtualMachinesController.java | 31 +-- .../properties/ImageConfigProperties.java | 38 ++++ .../project/service/ImageToolService.java | 6 +- .../service/ImageVirtualMachinesService.java | 30 +++ .../service/impl/ClientServiceImpl.java | 14 +- .../service/impl/ImageToolServiceImpl.java | 7 +- .../impl/ImageVirtualMachinesServiceImpl.java | 176 +++++++++++++++++- nex-be/src/main/resources/application.yml | 6 + 9 files changed, 332 insertions(+), 26 deletions(-) diff --git a/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java b/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java index f1d38a4..b589b95 100644 --- a/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java +++ b/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java @@ -11,11 +11,17 @@ import com.unisinsight.project.service.ImageToolService; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.io.File; import java.io.Serializable; import java.util.List; @@ -43,7 +49,7 @@ public class ImageToolController { /** * 服务对象 */ - @Resource + @javax.annotation.Resource private ImageToolService service; @Autowired private ChunkedUploadService chunkedUploadService; @@ -90,12 +96,12 @@ public class ImageToolController { @RequestParam("shard_total") int totalChunks, @RequestParam("file_name") String fileName, @RequestParam("file_size") long totalSize, - @RequestParam("file_version") String fileVersion, + @RequestParam(value = "file_version",required = false) String fileVersion, @RequestParam(value = "description", required = false) String description ) { return service.uploadChunk(chunk, chunkSize, chunkMd5, fileId, chunkNumber, totalChunks, - fileName, totalSize,description); + fileName, totalSize,description,fileType,fileVersion); } @PostMapping("/cancel/upload") @ApiOperation(value = "取消上传ImageTool文件") @@ -108,6 +114,42 @@ public class ImageToolController { return Result.errorResultMessage(BaseErrorCode.HTTP_ERROR_CODE_500, "取消上传失败: " + e.getMessage()); } } + + @GetMapping("/download/{id}") + @ApiOperation(value = "下载ImageTool文件", notes = "通过文件ID下载对应的ImageTool文件") + public ResponseEntity downloadFile(@PathVariable Integer id, HttpServletRequest request) { + try { + // 1. 查询文件信息 + ImageTool imageTool = service.getById(id); + if (imageTool == null || imageTool.getStorePath() == null) { + return ResponseEntity.notFound().build(); + } + + // 2. 获取文件路径 + String filePath = imageTool.getStorePath(); + File file = new File(filePath); + + if (!file.exists()) { + return ResponseEntity.notFound().build(); + } + + // 3. 构建响应 + FileSystemResource resource = new FileSystemResource(file); + String contentType = request.getServletContext().getMimeType(file.getAbsolutePath()); + if(contentType == null) { + contentType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + imageTool.getFileName() + "\"") + .body(resource); + } catch (Exception e) { + log.error("下载文件失败,id:{},error:{}", id, e.getMessage(), e); + return ResponseEntity.internalServerError().build(); + } + } + @ApiOperation(value = "修改") @PostMapping("/update") public Result updateUser(@RequestBody ImageToolReq imageToolReq) { diff --git a/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java b/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java index 5bba7fc..f2bc3d2 100644 --- a/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java +++ b/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java @@ -106,8 +106,26 @@ public class ImageVirtualMachinesController { return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); } log.info("终端启动请求参数为:{}", JSONUtil.toJsonStr(req)); - return service.cloneTemplate(req); + return service.start(req); } + @ApiOperation(value = "终端关闭") + @PostMapping("/shutdown") + public Result shutdown(@RequestBody ImageVirtualMachinesReq req) { + if (Objects.isNull(req)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("终端关闭请求参数为:{}", JSONUtil.toJsonStr(req)); + return service.shutdown(req); + } +// @ApiOperation(value = "终端强制关闭") +// @PostMapping("/destroy") +// public Result destroy(@RequestBody ImageVirtualMachinesReq req) { +// if (Objects.isNull(req)) { +// return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); +// } +// log.info("终端强制关闭请求参数为:{}", JSONUtil.toJsonStr(req)); +// return service.destroy(req); +// } @ApiOperation(value = "终端重启") @PostMapping("/reboot") public Result reboot(@RequestBody ImageVirtualMachinesReq req) { @@ -115,16 +133,7 @@ public class ImageVirtualMachinesController { return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); } log.info("终端重启请求参数为:{}", JSONUtil.toJsonStr(req)); - return service.cloneTemplate(req); - } - @ApiOperation(value = "终端快照") - @PostMapping("/snapshot") - public Result snapshot(@RequestBody ImageVirtualMachinesReq req) { - if (Objects.isNull(req)) { - return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); - } - log.info("终端快照请求参数为:{}", JSONUtil.toJsonStr(req)); - return service.cloneTemplate(req); + return service.reboot(req); } } diff --git a/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java b/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java index 5b5434b..9329491 100644 --- a/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java +++ b/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java @@ -43,6 +43,19 @@ public class ImageConfigProperties { private String updateUrl; @Getter(value=AccessLevel.NONE) private String startUrl; + @Getter(value=AccessLevel.NONE) + private String shutdownUrl; + @Getter(value=AccessLevel.NONE) + private String destroyUrl; + @Getter(value=AccessLevel.NONE) + private String rebootUrl; + @Getter(value=AccessLevel.NONE) + private String pauseUrl; + @Getter(value=AccessLevel.NONE) + private String resumeUrl; + @Getter(value=AccessLevel.NONE) + private String cloneToDesktopUrl; + @@ -65,4 +78,29 @@ public class ImageConfigProperties { public String getStartUrl() { return baseUrl+startUrl; } + + public String getShutdownUrl() { + return baseUrl+shutdownUrl; + } + + public String getDestroyUrl() { + return baseUrl+destroyUrl; + } + + public String getRebootUrl() { + return baseUrl+rebootUrl; + } + + public String getPauseUrl() { + return baseUrl+pauseUrl; + } + + public String getResumeUrl() { + return baseUrl+resumeUrl; + } + + public String getCloneToDesktopUrl() { + return baseUrl+cloneToDesktopUrl; + } + } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java b/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java index 3285d37..e5d74de 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java @@ -27,6 +27,10 @@ public interface ImageToolService extends IService { Result delete(DeleteIdReq deleteIdReq); - Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, int totalChunks, String fileName, long totalSize, String description); + Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, + String fileId, int chunkNumber, + int totalChunks, String fileName, + long totalSize, String description + , String fileType,String fileVersion); } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java b/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java index a501160..4d9da9a 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java @@ -27,5 +27,35 @@ public interface ImageVirtualMachinesService extends IService delete(DeleteIdReq deleteIdReq); Result cloneTemplate(ImageVirtualMachinesReq req); + + /** + * 启动虚拟机 + * @param req + * @return + */ + Result start(ImageVirtualMachinesReq req); + + /** + * 关闭虚拟机 + * @param req + * @return + */ + Result shutdown(ImageVirtualMachinesReq req); + + /** + * 强制关闭虚拟机 + * @param req + * @return + */ + Result destroy(ImageVirtualMachinesReq req); + + /** + * 重启虚拟机 + * @param req + * @return + */ + Result reboot(ImageVirtualMachinesReq req); + + } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java index dff5e09..aa49f75 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java @@ -6,9 +6,12 @@ import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.unisinsight.project.entity.dao.DeviceImageMapping; import com.unisinsight.project.entity.dao.Image; +import com.unisinsight.project.entity.dao.ImageDesktop; +import com.unisinsight.project.entity.res.ImageDesktopRes; import com.unisinsight.project.entity.res.ImageRes; import com.unisinsight.project.exception.Result; import com.unisinsight.project.mapper.DeviceImageMappingMapper; +import com.unisinsight.project.mapper.ImageDesktopMapper; import com.unisinsight.project.mapper.ImageMapper; import com.unisinsight.project.service.ClientService; import lombok.extern.slf4j.Slf4j; @@ -33,7 +36,7 @@ import java.util.stream.Collectors; public class ClientServiceImpl implements ClientService { @Resource - private ImageMapper imageMapper; + private ImageDesktopMapper imageDesktopMapper; @Resource private DeviceImageMappingMapper deviceImageMappingMapper; @@ -44,6 +47,7 @@ public class ClientServiceImpl implements ClientService { @Override public Result getImageList(String deviceId, String token) { +// todo 改为桌面镜像 需验证 HashMap hashMap = new HashMap<>(); List deviceImageMappings = deviceImageMappingMapper.selectList(new LambdaQueryWrapper().eq(DeviceImageMapping::getDeviceId, deviceId)); if (CollectionUtil.isEmpty(deviceImageMappings)) { @@ -52,13 +56,13 @@ public class ClientServiceImpl implements ClientService { } List imageIdList = deviceImageMappings.stream().map(DeviceImageMapping::getImageId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(imageIdList)) { - List images = imageMapper.selectList(new LambdaQueryWrapper().in(Image::getId, imageIdList)); + List images = imageDesktopMapper.selectList(new LambdaQueryWrapper().in(ImageDesktop::getId, imageIdList)); log.info("用户登录查询镜像结果:{}", JSONUtil.toJsonStr(images)); - List imageRes = BeanUtil.copyToList(images, ImageRes.class); + List imageRes = BeanUtil.copyToList(images, ImageDesktopRes.class); List> collect = imageRes.stream().distinct().map(e -> { HashMap map = new HashMap<>(); - if (StringUtils.isNotBlank(e.getImageFileName())) { - map.put("name", e.getImageFileName()); + if (StringUtils.isNotBlank(e.getDesktopName())) { + map.put("name", e.getDesktopName()); } if (StringUtils.isNotBlank(e.getBtPath())) { if (e.getBtPath().contains("http://") || e.getBtPath().contains("https://")) { diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java index 6470b17..9ab0ec0 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java @@ -123,7 +123,9 @@ public class ImageToolServiceImpl extends ServiceImpl uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, int totalChunks, String fileName, long totalSize, String description) { + public Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, + int totalChunks, String fileName, long totalSize, String description + , String fileType, String fileVersion) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ImageTool::getFileName, fileName); ImageTool exists = mapper.selectOne(queryWrapper); @@ -142,7 +144,8 @@ public class ImageToolServiceImpl extends ServiceImpl delete(DeleteIdReq deleteIdReq) { + // 先查询要删除的虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(deleteIdReq.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用镜像删除服务 + ImageDeleteReq deleteReq = ImageDeleteReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .deleteStorage(true) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(deleteReq, headers); + ResponseEntity exchange = restTemplate.exchange(imageConfigProperties.getDeleteUrl(), HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "删除虚拟机失败"); + } + + // 删除数据库记录 int deleted = machinesMapper.deleteById(deleteIdReq.getId()); log.info("终端删除delete:{}", deleted); if (deleted == 1) { @@ -243,8 +271,150 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl cloneTemplate(ImageVirtualMachinesReq req) { - //todo 调用镜像生成服务 在生成完毕后生成桌面镜像数据 桌面镜像加BT - return null; + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用克隆虚拟机到桌面镜像服务 + ImageCloneToDesktopReq cloneReq = ImageCloneToDesktopReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .desktopName(req.getImageName() + "_desktop") // 使用虚拟机名称加desktop作为桌面镜像名称 + .storagePath("/vms/iso") // 默认存储路径 + .description("从虚拟机克隆的桌面镜像") + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(cloneReq, headers); + String cloneToDesktopUrl = imageConfigProperties.getCloneToDesktopUrl(); + ResponseEntity exchange = restTemplate.exchange(cloneToDesktopUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "克隆虚拟机到桌面镜像失败"); + } + + // 克隆成功后生成桌面镜像数据 + ImageDesktop imageDesktop = new ImageDesktop(); + imageDesktop.setDesktopName(req.getImageName() + "_desktop"); + imageDesktop.setImageVirtualId(Math.toIntExact(req.getId())); + imageDesktop.setOsVersion(imageVirtualMachines.getOsVersion()); +// imageDesktop.setStoragePath("/vms/iso/" + req.getImageName() + "_desktop.qcow2"); // 假设是qcow2格式 +// imageDesktop.setDesktopType(3); // QCOW2格式 +// imageDesktop.setPublishStatus("unpublished"); // 默认未发布状态 + imageDesktop.setCreateUser("admin"); + imageDesktop.setUpdateUser("admin"); + imageDesktop.setDescription("从虚拟机克隆的桌面镜像"); + + // 保存桌面镜像数据 + boolean saved = imageDesktopService.save(imageDesktop); + if (!saved) { + log.error("保存桌面镜像数据失败"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "保存桌面镜像数据失败"); + } + + return Result.successResult(); } + + @Override + public Result start(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用启动虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + ResponseEntity exchange = restTemplate.exchange(imageConfigProperties.getStartUrl(), HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "启动虚拟机失败"); + } + + return Result.successResult(); + } + + @Override + public Result shutdown(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用关闭虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + String shutdownUrl = imageConfigProperties.getShutdownUrl(); + ResponseEntity exchange = restTemplate.exchange(shutdownUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "关闭虚拟机失败"); + } + + return Result.successResult(); + } + + @Override + public Result destroy(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用强制关闭虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + String destroyUrl = imageConfigProperties.getDestroyUrl(); + ResponseEntity exchange = restTemplate.exchange(destroyUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "强制关闭虚拟机失败"); + } + + return Result.successResult(); + } + + @Override + public Result reboot(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用重启虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + String rebootUrl = imageConfigProperties.getRebootUrl(); + ResponseEntity exchange = restTemplate.exchange(rebootUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "重启虚拟机失败"); + } + + return Result.successResult(); + } + } diff --git a/nex-be/src/main/resources/application.yml b/nex-be/src/main/resources/application.yml index 608d2dd..7f8db3e 100644 --- a/nex-be/src/main/resources/application.yml +++ b/nex-be/src/main/resources/application.yml @@ -15,6 +15,12 @@ image: delete-url: /api/v1/vm/delete update-url: /api/v1/vm/update start-url: /api/v1/vm/start + shutdown-url: /api/v1/vm/shutdown + destroy-url: /api/v1/vm/destroy + reboot-url: /api/v1/vm/reboot + pause-url: /api/v1/vm/pause + resume-url: /api/v1/vm/resume + clone-to-desktop-url: /api/v1/vm/clone-to-desktop spring: servlet: multipart: