Merge remote-tracking branch 'origin/master'

master
汤全昆 2025-09-02 10:03:08 +08:00
commit f2b298f9c7
25 changed files with 1696 additions and 33 deletions

View File

@ -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);
}
}

View File

@ -0,0 +1,183 @@
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.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.servlet.http.HttpServletRequest;
import java.io.File;
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 {
/**
*
*/
@javax.annotation.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(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,fileType,fileVersion);
}
@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());
}
}
@GetMapping("/download/{id}")
@ApiOperation(value = "下载ImageTool文件", notes = "通过文件ID下载对应的ImageTool文件")
public ResponseEntity<Resource> 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) {
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);
}
}

View File

@ -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);
}
}

View File

@ -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<ImageTool> {
/**
* ${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;
}

View File

@ -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
* <pre> Copyright: Copyright(c) 2025 </pre>
* <pre> Company : </pre>
* 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;
}
}

View File

@ -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")

View File

@ -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;
}

View File

@ -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;
/**
*
**/

View File

@ -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;
}

View File

@ -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<ImageTool> {
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<String, Object> 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;
}
}

View File

@ -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<ChunkedUploadCompletion> 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;
}

View File

@ -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;
}
}

View File

@ -22,4 +22,8 @@ public interface ClientOperateService {
Result<?> terminalStart(DeviceReq deviceReq);
Result<?> terminalEnd(DeviceReq deviceReq);
Result<?> usbEnabled(DeviceReq deviceReq);
Result<?> usbDisabled(DeviceReq deviceReq);
}

View File

@ -0,0 +1,36 @@
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<ImageTool> {
Result<PageResult<ImageToolRes>> 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
, String fileType,String fileVersion);
}

View File

@ -27,5 +27,35 @@ public interface ImageVirtualMachinesService extends IService<ImageVirtualMachin
Result<?> 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);
}

View File

@ -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<String, FileUploadInfo> 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<ChunkedUploadCompletion> 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<Integer> 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<Integer> getUploadedChunks() {
return uploadedChunks;
}
public int getTotalChunks() {
return totalChunks;
}
}
}

View File

@ -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());
}
}

View File

@ -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<String, Object> hashMap = new HashMap<>();
List<DeviceImageMapping> deviceImageMappings = deviceImageMappingMapper.selectList(new LambdaQueryWrapper<DeviceImageMapping>().eq(DeviceImageMapping::getDeviceId, deviceId));
if (CollectionUtil.isEmpty(deviceImageMappings)) {
@ -52,13 +56,13 @@ public class ClientServiceImpl implements ClientService {
}
List<Long> imageIdList = deviceImageMappings.stream().map(DeviceImageMapping::getImageId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(imageIdList)) {
List<Image> images = imageMapper.selectList(new LambdaQueryWrapper<Image>().in(Image::getId, imageIdList));
List<ImageDesktop> images = imageDesktopMapper.selectList(new LambdaQueryWrapper<ImageDesktop>().in(ImageDesktop::getId, imageIdList));
log.info("用户登录查询镜像结果:{}", JSONUtil.toJsonStr(images));
List<ImageRes> imageRes = BeanUtil.copyToList(images, ImageRes.class);
List<ImageDesktopRes> imageRes = BeanUtil.copyToList(images, ImageDesktopRes.class);
List<HashMap<String, Object>> collect = imageRes.stream().distinct().map(e -> {
HashMap<String, Object> 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://")) {

View File

@ -0,0 +1,219 @@
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<ImageToolMapper, ImageTool> 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<PageResult<ImageToolRes>> selectPage(ImageToolReq imageToolReq) {
Page<ImageTool> page = new Page<>(imageToolReq.getPageNum(), imageToolReq.getPageSize());
LambdaQueryWrapper<ImageTool> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByAsc(ImageTool::getId);
Page<ImageTool> imageToolPage = mapper.selectPage(page, queryWrapper);
log.info("分页查询返回:{}", JSONUtil.toJsonStr(imageToolPage));
if (CollectionUtil.isEmpty(imageToolPage.getRecords())) {
log.info("分页查询返回为空");
return Result.successResult();
} else {
PageResult<ImageToolRes> convert = PageResult.convertIPage(imageToolPage, ImageToolRes.class);
List<ImageToolRes> 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
, String fileType, String fileVersion) {
LambdaQueryWrapper<ImageTool> 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<String, Object> 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.setFileType(fileType);
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 "";
}
}

View File

@ -9,9 +9,14 @@ 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.dao.ImageDesktop;
import com.unisinsight.project.entity.req.DeleteIdReq;
import com.unisinsight.project.entity.req.ImageCloneToDesktopReq;
import com.unisinsight.project.entity.req.ImageCreateReq;
import com.unisinsight.project.entity.req.ImageDeleteReq;
import com.unisinsight.project.entity.req.ImageOperationReq;
import com.unisinsight.project.entity.req.ImageUpdateReq;
import com.unisinsight.project.entity.req.ImageVirtualMachinesReq;
import com.unisinsight.project.entity.res.ImageStatusRes;
@ -21,7 +26,9 @@ import com.unisinsight.project.exception.BaseErrorCode;
import com.unisinsight.project.exception.Result;
import com.unisinsight.project.mapper.ImageVirtualMachinesMapper;
import com.unisinsight.project.properties.ImageConfigProperties;
import com.unisinsight.project.service.ImageDesktopService;
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 +61,10 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
private RestTemplate restTemplate;
@Autowired
private ImageConfigProperties imageConfigProperties;
@Resource
private ImageToolService imageToolService;
@Autowired
private ImageDesktopService imageDesktopService;
@Override
@ -124,11 +134,26 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override
public Result<?> insert(ImageVirtualMachinesReq imageVirtualMachinesReq) {
QueryWrapper<ImageVirtualMachines> 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<Image> imageLambdaQueryWrapper = new LambdaQueryWrapper<>();
imageLambdaQueryWrapper.in(Image::getId, imageVirtualMachinesReq.getImageSystemId());
Image systemImage = imageService.getOne(imageLambdaQueryWrapper);
//查询驱动信息
LambdaQueryWrapper<ImageTool> 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,11 +162,11 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
.memory(imageVirtualMachinesReq.getMemoryTotal())
.diskSize(imageVirtualMachinesReq.getSystemTotal())
//存储池
// .storagePool(imageVirtualMachinesReq.getStoragePath())
.storagePoolName(imageVirtualMachinesReq.getStoragePoolName())
.networkName(imageVirtualMachinesReq.getNetworkModule())
.isoPath(imageVirtualMachinesReq.getStoragePath())
//驱动
// .virtioWinPath(imageVirtualMachinesReq.getStoragePath())
.virtioWinPath(imageTool.getStorePath())
.description(imageVirtualMachinesReq.getDescription())
.build();
@ -154,12 +179,7 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
}
ImageVirtualMachines imageVirtualMachines = BeanUtil.copyProperties(imageVirtualMachinesReq, ImageVirtualMachines.class);
QueryWrapper<ImageVirtualMachines> 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 +187,7 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
if (insert == 1) {
return Result.successResult();
} else {
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500);
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500,"新增失败");
}
}
@ -219,6 +239,27 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override
public Result<?> 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<ImageDeleteReq> requestEntity = new HttpEntity<>(deleteReq, headers);
ResponseEntity<ImageStatusRes> 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) {
@ -230,8 +271,150 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override
public Result<?> 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<ImageCloneToDesktopReq> requestEntity = new HttpEntity<>(cloneReq, headers);
String cloneToDesktopUrl = imageConfigProperties.getCloneToDesktopUrl();
ResponseEntity<ImageStatusRes> 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<ImageOperationReq> requestEntity = new HttpEntity<>(operationReq, headers);
ResponseEntity<ImageStatusRes> 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<ImageOperationReq> requestEntity = new HttpEntity<>(operationReq, headers);
String shutdownUrl = imageConfigProperties.getShutdownUrl();
ResponseEntity<ImageStatusRes> 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<ImageOperationReq> requestEntity = new HttpEntity<>(operationReq, headers);
String destroyUrl = imageConfigProperties.getDestroyUrl();
ResponseEntity<ImageStatusRes> 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<ImageOperationReq> requestEntity = new HttpEntity<>(operationReq, headers);
String rebootUrl = imageConfigProperties.getRebootUrl();
ResponseEntity<ImageStatusRes> 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();
}
}

View File

@ -6,13 +6,21 @@ file:
temp-dir: /var/lib/vdi/tmp/chunked-uploads
dir: /var/lib/vdi/test
bt-url: http://10.100.51.86:8114
tool:
dir: /vms/tool/iso
image:
base-url: http://10.100.51.118:5173
base-url: http://10.100.51.178:5173
status-url: /api/v1/vm/batch-status
create-url: /api/v1/vm/create
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:

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unisinsight.project.mapper.ImageToolMapper">
<resultMap type="com.unisinsight.project.entity.dao.ImageTool" id="ImageToolMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="fileName" column="file_name" jdbcType="VARCHAR"/>
<result property="fileType" column="file_type" jdbcType="VARCHAR"/>
<result property="fileSize" column="file_size" jdbcType="INTEGER"/>
<result property="fileVersion" column="file_version" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="VARCHAR"/>
<result property="createUser" column="create_user" jdbcType="VARCHAR"/>
<result property="updateTime" column="update_time" jdbcType="VARCHAR"/>
<result property="updateUser" column="update_user" jdbcType="VARCHAR"/>
<result property="uploadTime" column="upload_time" jdbcType="VARCHAR"/>
<result property="storePath" column="store_path" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id, file_name, file_type, file_size, file_version, create_time, create_user, update_time, update_user, upload_time, store_path, description
</sql>
</mapper>