断点续传+测试方法完整示例
因为看不懂网上的断点续传案例,而且又不能直接复制使用,干脆自己想想写了一个。
上传入参类:
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;/*** 断点续传接收实体类** @author kwy* @date 2024/11/18*/
@Data
@ApiModel("镜像文件断点续传接收实体类")
public class ImageFileBreakpointResumeDTO {@ApiModelProperty(value = "上传的文件流")@NotNull(message = "上传的文件流不能为空")@JsonIgnoreprivate MultipartFile multipartFile;@ApiModelProperty(value = "云命名空间")@NotBlank(message = "云命名空间不能为空")private String namespace;@ApiModelProperty(value = "镜像名称")@NotBlank(message = "镜像名称不能为空")private String name;@ApiModelProperty(value = "版本号")@NotBlank(message = "版本号不能为空")private String version;@ApiModelProperty(value = "文件上传标识")@NotBlank(message = "文件上传标识不能为空")private String taskId;@ApiModelProperty(value = "分片总数")@NotNull(message = "分片总数不能为空")private Integer numTotal;@ApiModelProperty(value = "上传到第几片")@NotNull(message = "上传到第几片不能为空")private Integer uploadNum;@ApiModelProperty(value = "文件名称")@NotBlank(message = "文件名称不能为空")private String fileName;@ApiModelProperty(value = "是否上传成功", hidden = true)private Boolean status;@ApiModelProperty(value = "分片文件路径", hidden = true)private String filePath;
}
controller层:
@PostMapping("/breakpointResumeFile")@ApiOperation(value = "断点续传|重试上传镜像文件")public ResponseData<String> breakpointResumeFile(@Validated ImageFileBreakpointResumeDTO dto) {return ResponseData.ok(disImageMgrService.breakpointResumeFile(dto));}@PostMapping("/test")@ApiOperation(value = "断点续传|重试上传镜像文件 - 测试")public ResponseData<Boolean> test() {disImageMgrService.test();return ResponseData.ok();}/*** 为避免本地临时文件过多,清除临时分片文件* ps:请勿在用户上传的期间操作*/@ApiOperation(value = "清除临时分片文件")@PostMapping("/clearItemFile")public ResponseData<Boolean> clearItemFile() {disImageMgrService.clearItemFile();return ResponseData.ok();}
service层:
接口
/*** 上传镜像文件** @param dto 文件参数内容* @return 合并时返回dockerFile值,否则返回null*/String breakpointResumeFile(ImageFileBreakpointResumeDTO dto);/*** 测试断点续传*/void test();/*** 为避免本地临时文件过多,清除临时分片文件* ps:请勿在用户上传的期间操作*/void clearItemFile();
实现方法
@Overridepublic String breakpointResumeFile(ImageFileBreakpointResumeDTO dto) {MultipartFile multipartFile = dto.getMultipartFile();// 校验if (multipartFile.getSize() <= 0) {throw new CommonException("无效文件");}String taskName = "IMAGE_BREAKPOINT_RESUME_TASK_ID_" + dto.getTaskId();StringBuilder path = new StringBuilder("/imageBreakpointResumeStorage/");path.append(dto.getNamespace()).append("/").append(dto.getName()).append("/").append(dto.getVersion()).append("/").append(dto.getTaskId());File directory = new File(path.toString());if (!directory.exists()) {directory.mkdirs();}// 本次切片文件String filePath = path + "/" + multipartFile.getOriginalFilename();dto.setFilePath(filePath);Map<String, String> allRecordMap = new HashMap<>();try {// 1.判断任务是否存在(分片存储到临时目录,临时目录应定时清空,以防被垃圾分片占满,完成了再拉下来合并)if (Boolean.FALSE.equals(redisUtil.hasKey(taskName))) {allRecordMap = new HashMap<>();} else {String taskJson = redisUtil.get(taskName, String.class);if (StringUtils.isNotBlank(taskJson)) {allRecordMap = (Map<String, String>) JSONUtil.toBean(taskJson, Map.class);}}// 如果文件片存在,则认为本次是重新上传String recordJson = allRecordMap.get(dto.getUploadNum().toString());if (StringUtils.isNotBlank(recordJson)) {// 删除旧的文件FileUtil.deleteFile(filePath);}// 2.保存分片到临时目录FileUtil.uploadSingleFile(dto.getMultipartFile(), filePath);// 2.1 记录本次分片上传dto.setStatus(true);allRecordMap.put(dto.getUploadNum().toString(), JSONUtil.toJsonStr(dto));Boolean result = redisUtil.set(taskName, JSONUtil.toJsonStr(allRecordMap), 1L, TimeUnit.DAYS);if (Boolean.FALSE.equals(result)) {throw new CommonException("记录本次操作失败");}// 3.判断 文件切片上传成功数===文件总切片数,合并切片到成品目录,返回文件上传成功int successNum = 0;List<ImageFileBreakpointResumeDTO> fileList = new ArrayList<>();for (Map.Entry<String, String> entry : allRecordMap.entrySet()) {String json = entry.getValue();ImageFileBreakpointResumeDTO bean = JSONUtil.toBean(json, ImageFileBreakpointResumeDTO.class);if (Boolean.TRUE.equals(bean.getStatus())) {successNum++;fileList.add(bean);}}if (successNum == dto.getNumTotal()) {fileList.sort(Comparator.comparingInt(ImageFileBreakpointResumeDTO::getUploadNum));File finishFile = new File(path + "/" + dto.getFileName());if (finishFile.exists()) {// 如果存在则先删除FileUtils.forceDelete(finishFile);}for (ImageFileBreakpointResumeDTO item : fileList) {File itemFile = new File(item.getFilePath());FileUtils.writeByteArrayToFile(finishFile, Files.toByteArray(itemFile), true);// 删除临时文件FileUtils.forceDelete(itemFile);}// 删除上传任务redisUtil.del(taskName);return finishFile.getPath();}} catch (Exception e) {dto.setStatus(false);allRecordMap.put(dto.getUploadNum().toString(), JSONUtil.toJsonStr(dto));redisUtil.set(taskName, JSONUtil.toJsonStr(allRecordMap), 1L, TimeUnit.DAYS);LogTraceUtil.error(e);throw new CommonException("上传任务失败", e.getMessage());} finally {System.out.println("--------------------------");System.out.println(JSONUtil.toJsonStr(dto));}return null;}public static File[] splitFile(File file, int numberOfShards) throws IOException {long fileSize = file.length();long shardSize = fileSize / numberOfShards;long remainder = fileSize % numberOfShards;File[] shards = new File[numberOfShards];File shardDir = new File("D:\\work\\shards");if (!shardDir.exists()) {shardDir.mkdirs();}try (FileInputStream fis = new FileInputStream(file)) {byte[] buffer = new byte[1024];for (int i = 0; i < numberOfShards; i++) {File shardFile = new File(shardDir, "shard_" + (i + 1) + ".part");shards[i] = shardFile;try (FileOutputStream fos = new FileOutputStream(shardFile)) {long bytesToWrite = shardSize;if (remainder > 0) {bytesToWrite++;remainder--;}long bytesCopied = 0;while (bytesCopied < bytesToWrite) {int bytesReadThisTime = fis.read(buffer, 0, (int) Math.min(buffer.length, bytesToWrite - bytesCopied));if (bytesReadThisTime == -1) {throw new IOException("Unexpected end of file while writing shard " + (i + 1));}fos.write(buffer, 0, bytesReadThisTime);bytesCopied += bytesReadThisTime;}}}}return shards;}@Overridepublic void test() {File fileToSplit = new File("D:\\work\\凯通科技-协同子系统\\distributed\\协同构建子系统-界面原型.rp");//File fileToSplit = new File("C:\\Users\\kewenyang\\Desktop\\index.vue");try {int index = 4;File[] shards = splitFile(fileToSplit, index);String id = UUID.randomUUID().toString();for (int i = 0; i < index; i++) {File shard = shards[i];MultipartFile multipartFile = IMultipartFileImpl.fileToMultipartFile(shard, MediaType.APPLICATION_OCTET_STREAM_VALUE);if (multipartFile.getSize() <= 0) {continue;}ImageFileBreakpointResumeDTO dto = new ImageFileBreakpointResumeDTO();dto.setUploadNum(i);dto.setNamespace("www.baidu.com");dto.setName("baidu");dto.setVersion("1.0.0");dto.setTaskId(id);dto.setMultipartFile(multipartFile);dto.setNumTotal(index);dto.setFileName("协同构建子系统-界面原型.rp");//dto.setFileName("index.vue");this.breakpointResumeFile(dto);System.out.println("Shard created: " + shard.getAbsolutePath());}} catch (IOException e) {e.printStackTrace();}}@Overridepublic void clearItemFile() {Path rootDir = Paths.get("/imageBreakpointResumeStorage/");try {java.nio.file.Files.walkFileTree(rootDir, new SimpleFileVisitor<Path>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {if (file.toString().endsWith(".part")) {java.nio.file.Files.delete(file);System.out.println("Deleted file: " + file);}return FileVisitResult.CONTINUE;}@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {// 如果在访问目录内容之前对其进行处理,可以在这里添加逻辑。return FileVisitResult.CONTINUE;}@Overridepublic FileVisitResult visitFileFailed(Path file, IOException exc) {System.err.println("Failed to visit file: " + file + " (reason: " + exc.getMessage() + ")");return FileVisitResult.CONTINUE;}});} catch (IOException e) {throw new CommonException("执行定时清除分片文件失败");}}
工具类:
redisUtil
import cn.hutool.json.JSONObject;
import com.cttnet.common.util.LogTraceUtil;
import com.cttnet.microservices.techteam.distributed.deploy.exception.CommonException;
import com.google.common.collect.Lists;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;/*** redis 工具类** @author kwy* @date 2024/11/20*/
@Component
@Slf4j
public class RedisUtil {@Resourceprivate ModelMapper modelMapper;@Resourceprivate RedisTemplate<String, Object> redisTemplate;// =============================common============================/*** 指定缓存失效时间** @param key 缓存的键* @param second 缓存失效时间(秒)* @return 设置是否成功*/public Boolean expire(String key, Long second) {return expire(key, second, TimeUnit.SECONDS);}/*** 指定缓存失效时间** @param key 缓存的键* @param time 缓存失效时间* @param timeUnit 时间单位* @return 设置是否成功*/public Boolean expire(String key, Long time, TimeUnit timeUnit) {try {if (time > 0) {redisTemplate.expire(key, time, timeUnit);}return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 根据key 获取过期时间** @param key 键 不能为null* @return 时间(秒) 返回0代表为永久有效*/public Long getExpire(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);}/*** 判断key是否存在** @param key 键* @return true 存在 false不存在*/public Boolean hasKey(String key) {try {return redisTemplate.hasKey(key);} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 设置字符串键值对。* 如果提供的值为null,则将其视为空字符串""进行处理。** @param key 键名,指定要设置的Redis键。* @param value 值,如果为null,则设置为空字符串""。* @return 如果设置成功,则返回true;如果设置失败(例如由于异常),则返回false。*/public Boolean set(String key, Object value) {try {value = null == value ? "" : value;redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 设置带有过期时间的字符串键值对。** @param key 键名,指定要设置的Redis键。* @param value 值,如果值为null,则默认设置为空字符串""。* @param time 过期时间。* @param timeUnit 时间单位。* @return 如果设置成功,则返回true;如果设置失败(例如由于异常),则返回false。*/public Boolean set(String key, Object value, Long time, TimeUnit timeUnit) {try {value = null == value ? "" : value;redisTemplate.opsForValue().set(key, value, time, timeUnit);return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 删除缓存** @param key 可以传一个值 或多个*/public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);} else {redisTemplate.delete(Lists.newArrayList(key));}}}/*** 删除对应的value*/public void remove(final String key) {if (Boolean.TRUE.equals(exists(key))) {redisTemplate.delete(key);}}/*** 删除hash结构中的某个字段*/public void hashDel(final String key, String item) {if (Boolean.TRUE.equals(exists(key))) {redisTemplate.opsForHash().delete(key, item);}}/*** 判断缓存中是否有对应的value*/public Boolean exists(final String key) {return redisTemplate.hasKey(key);}// ============================String=============================/*** 设置 String 类型键值对。** @param key 键名。* @param value 值,如果值为空或仅包含空白字符,则将其设置为空字符串。* @return 设置成功返回 true,否则返回 false。*/public Boolean strSet(String key, String value) {try {value = StringUtils.isBlank(value) ? "" : value;redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 设置带过期时间的 String 类型键值对。** @param key 键名。* @param value 值,如果值为空或仅包含空白字符,则将其设置为空字符串。* @param time 过期时间。* @param timeUnit 时间单位。* @return 设置成功返回 true,否则返回 false。*/public Boolean strSet(String key, String value, Long time, TimeUnit timeUnit) {try {value = StringUtils.isBlank(value) ? "" : value;redisTemplate.opsForValue().set(key, value, time, timeUnit);return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 根据给定的键名从 Redis 中获取对应的值。** @param key 键名。* @return 如果键名存在,则返回对应的值;如果键名不存在或键名为空,则返回 null。*/public Object get(String key) {if (StringUtils.isBlank(key)) {return null;}return redisTemplate.opsForValue().get(key);}/*** 根据给定的键名从 Redis 中获取对应的值,并将该值转换为指定类型的对象。* 如果值不是基本数据类型(String、Integer、Double、Byte),则使用 JSON 反序列化将其转换为指定类型的对象。** @param key 键名。* @param clazz 指定转换后的对象类型。* @return 如果键名存在,则返回对应的对象;如果键名不存在或键名为空,则返回 null。*/public <T> T get(String key, Class<T> clazz) {if (StringUtils.isBlank(key)) {return null;}if (clazz.equals(String.class)|| clazz.equals(Integer.class)|| clazz.equals(Double.class)|| clazz.equals(Byte.class)) {return (T) redisTemplate.opsForValue().get(key);}JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(key);if (null == jsonObject) {return null;}return jsonObject.toBean(clazz);}/*** 对存储在指定键的数值执行原子递增操作。** @param key 键名,指定要递增的 Redis 键。* @param delta 递增量,表示要增加的值(必须大于0)。* @return 递增后的值。* @throws RuntimeException 如果递增量小于等于0,则抛出此异常。*/public Long incr(String key, Long delta) {if (delta < 0) {throw new CommonException("递增因子必须大于0");}return redisTemplate.opsForValue().increment(key, delta);}/*** 对存储在指定键的数值执行原子递减操作。** @param key 键名,指定要递减的 Redis 键。* @param delta 递减量,表示要减少的值(必须大于0)。* @return 递减后的值。* @throws RuntimeException 如果递减量小于等于0,则抛出此异常。*/public Long decr(String key, Long delta) {if (delta < 0) {throw new CommonException("递减因子必须大于0");}return redisTemplate.opsForValue().increment(key, -delta);}// ================================Map=================================/*** HashGet** @param key 键 不能为null* @param item 项 不能为null* @return 值*/public Object hget(String key, String item) {return redisTemplate.opsForHash().get(key, item);}/*** 获取hashKey对应的所有键值** @param key 键* @return 对应的多个键值*/public Map<Object, Object> hmget(String key) {return redisTemplate.opsForHash().entries(key);}/*** 获取hashKey对应的所有键值** @param key 键* @return 对应的多个键值*/public <K, V> Map<K, V> hmget(String key, Class<K> k, Class<V> v) {Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);return (Map<K, V>) entries;}/*** 获取hashKey对应的所有键值** @param key 键* @param clazz 类* @return 对应的多个键值*/public <T> T hmget(String key, Class<T> clazz) {Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);return modelMapper.map(entries, clazz);}/*** HashSet** @param key 键* @param map 对应多个键值* @return true 成功 false 失败*/public Boolean hmset(String key, Map map) {try {redisTemplate.opsForHash().putAll(key, map);return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** HashSet 并设置时间** @param key 键* @param map 对应多个键值* @param time 时间(秒)* @return true成功 false失败*/public Boolean hmset(String key, Map<String, Object> map, Long time) {try {redisTemplate.opsForHash().putAll(key, map);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** HashSet 并设置时间** @param key 键* @param map 对应多个键值* @param time 时间* @param timeUnit 单位* @return true成功 false失败*/public Boolean hmset(String key, Map<String, Object> map, Long time, TimeUnit timeUnit) {try {redisTemplate.opsForHash().putAll(key, map);if (time > 0) {expire(key, time, timeUnit);}return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 向一张hash表中放入数据,如果不存在将创建** @param key 键* @param item 项* @param value 值* @return true 成功 false失败*/public Boolean hset(String key, String item, Object value) {try {redisTemplate.opsForHash().put(key, item, value);return true;} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 向一张hash表中放入数据,如果不存在将创建** @param key 键* @param item 项* @param value 值* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间* @return true 成功 false失败*/public Boolean hset(String key, String item, Object value, Long time) {try {redisTemplate.opsForHash().put(key, item, value);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {LogTraceUtil.error(e);}return false;}/*** 删除hash表中的值** @param key 键 不能为null* @param item 项 可以使多个 不能为null*/public void hdel(String key, Object... item) {redisTemplate.opsForHash().delete(key, item);}/*** 判断hash表中是否有该项的值** @param key 键 不能为null* @param item 项 不能为null* @return true 存在 false不存在*/public Boolean hHasKey(String key, String item) {return redisTemplate.opsForHash().hasKey(key, item);}/*** 对存储在指定键的哈希表中某个字段的值执行原子递增操作。* 如果哈希表或字段不存在,则会创建一个新的哈希表或字段,并将值初始化为0,然后执行递增操作。** @param key 键名,指定要操作的 Redis 哈希表键。* @param item 字段名,指定要递增的哈希表字段。* @param by 递增量,表示要增加的值(必须大于0)。* @return 递增后的值。*/public double hincr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, by);}/*** hash递减** @param key 键* @param item 项* @param by 要减少记(小于0)* @return 递减后的值。*/public double hdecr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, -by);}// ============================set=============================/*** 根据key获取Set中的所有值** @param key 键* @return 返回一个包含集合中所有元素的集合。如果发生异常,则返回 null。*/public Set<Object> sGet(String key) {try {return redisTemplate.opsForSet().members(key);} catch (Exception e) {LogTraceUtil.error(e);return null;}}/*** 根据value从一个set中查询,是否存在** @param key 键* @param value 值* @return true 存在 false不存在*/public Boolean sHasKey(String key, Object value) {try {return redisTemplate.opsForSet().isMember(key, value);} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 将数据放入set缓存** @param key 键* @param values 值 可以是多个* @return 成功个数*/public Long sSet(String key, Object... values) {try {return redisTemplate.opsForSet().add(key, values);} catch (Exception e) {LogTraceUtil.error(e);return 0L;}}/*** 将set数据放入缓存** @param key 键* @param second 时间(秒)* @param values 值 可以是多个* @return 成功个数*/public Long sSetAndTime(String key, Long second, Object... values) {return sSetAndTime(key, second, TimeUnit.SECONDS, values);}/*** 将set数据放入缓存** @param key 键* @param time 时间* @param values 值 可以是多个* @return 成功个数*/public Long sSetAndTime(String key, Long time, TimeUnit timeUnit, Object... values) {try {Long count = redisTemplate.opsForSet().add(key, values);if (time > 0) {expire(key, time, timeUnit);}return count;} catch (Exception e) {LogTraceUtil.error(e);return 0L;}}/*** 获取set缓存的长度** @param key 键* @return 返回集合中元素的数量。如果发生异常,则返回 0。*/public Long sGetSetSize(String key) {try {return redisTemplate.opsForSet().size(key);} catch (Exception e) {LogTraceUtil.error(e);return 0L;}}/*** 移除值为value的** @param key 键* @param values 值 可以是多个* @return 移除的个数*/public Long setRemove(String key, Object... values) {try {return redisTemplate.opsForSet().remove(key, values);} catch (Exception e) {LogTraceUtil.error(e);return 0L;}}// ===============================list=================================/*** 获取list缓存的内容** @param key 键* @param start 开始* @param end 结束 0 到 -1代表所有值* @return 返回一个包含指定范围内元素的列表。如果发生异常,则返回 null。*/public List<Object> lGet(String key, Long start, Long end) {try {return redisTemplate.opsForList().range(key, start, end);} catch (Exception e) {LogTraceUtil.error(e);return null;}}/*** 获取list缓存的长度** @param key 键* @return 返回列表中元素的数量。如果发生异常,则返回 0。*/public Long lGetListSize(String key) {try {return redisTemplate.opsForList().size(key);} catch (Exception e) {LogTraceUtil.error(e);return 0L;}}/*** 通过索引 获取list中的值** @param key 键* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推* @return 返回指定索引处的元素值。如果索引超出范围或发生异常,则返回 null。*/public Object lGetIndex(String key, Long index) {try {return redisTemplate.opsForList().index(key, index);} catch (Exception e) {LogTraceUtil.error(e);return null;}}/*** 将单个对象添加到 Redis 列表的右侧。** @param key 键名,指定要操作的 Redis 列表键。* @param value 要添加到列表末尾的对象。* @return 如果操作成功,则返回 true;否则返回 false。*/public Boolean lSet(String key, Object value) {try {redisTemplate.opsForList().rightPush(key, value);return true;} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 将list放入缓存** @param key 键* @param value 值* @param time 时间(秒)* @return 如果操作成功,则返回 true;否则返回 false。*/public Boolean lSet(String key, Object value, Long time) {try {redisTemplate.opsForList().rightPush(key, value);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 将list放入缓存** @param key 键* @param value 值*/public Boolean lSet(String key, List<Object> value) {try {redisTemplate.opsForList().rightPushAll(key, value);return true;} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 将list放入缓存** @param key 键* @param value 值* @param time 时间(秒)* @return 如果操作成功,则返回 true;否则返回 false。*/public Boolean lSet(String key, List<Object> value, Long time) {try {redisTemplate.opsForList().rightPushAll(key, value);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 根据索引修改list中的某条数据** @param key 键* @param index 索引* @param value 值* @return 如果操作成功,则返回 true;否则返回 false。*/public Boolean lUpdateIndex(String key, Long index, Object value) {try {redisTemplate.opsForList().set(key, index, value);return true;} catch (Exception e) {LogTraceUtil.error(e);return false;}}/*** 移除N个值为value** @param key 键* @param count 移除多少个* @param value 值* @return 移除的个数*/public Long lRemove(String key, Long count, Object value) {try {return redisTemplate.opsForList().remove(key, count, value);} catch (Exception e) {LogTraceUtil.error(e);return 0L;}}/*** 获取固定前缀的key** @param suffix 键名的后缀,用于匹配所有以该后缀开头的键名。* @return 一个包含所有匹配键名的集合。*/public Set<String> getKeySuffix(String suffix) {return redisTemplate.keys(suffix + ":*");}}
文件转换类(用于本地测试)
import com.cttnet.microservices.techteam.distributed.deploy.exception.CommonException;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;/*** 负责将InputStream转换MultipartFile,可以少引一个jar包,本来用的是spring-test-5.2.8中的MockMultipartFile,直接提取出来使用* 见 https://blog.csdn.net/m0_37609579/article/details/100901358** @author kwy* @date 2024/11/20*/
public class IMultipartFileImpl implements MultipartFile {/*** 文件名称*/private final String name;/*** 原始文件名*/private final String originalFilename;/*** 文件内容类型*/@Nullableprivate final String contentType;/*** 文件内容字节数组*/private final byte[] content;/*** Create a new MockMultipartFile with the given content.** @param name the name of the file* @param content the content of the file*/public IMultipartFileImpl(String name, @Nullable byte[] content) {this(name, "", null, content);}/*** Create a new MockMultipartFile with the given content.** @param name the name of the file* @param contentStream the content of the file as stream* @throws IOException if reading from the stream failed*/public IMultipartFileImpl(String name, InputStream contentStream) throws IOException {this(name, "", null, FileCopyUtils.copyToByteArray(contentStream));}/*** Create a new MockMultipartFile with the given content.** @param name the name of the file* @param originalFilename the original filename (as on the client's machine)* @param contentType the content type (if known)* @param content the content of the file*/public IMultipartFileImpl(String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) {Assert.hasLength(name, "Name must not be empty");this.name = name;this.originalFilename = (originalFilename != null ? originalFilename : "");this.contentType = contentType;this.content = (content != null ? content : new byte[0]);}/*** Create a new MockMultipartFile with the given content.** @param name the name of the file* @param originalFilename the original filename (as on the client's machine)* @param contentType the content type (if known)* @param contentStream the content of the file as stream* @throws IOException if reading from the stream failed*/public IMultipartFileImpl(String name, @Nullable String originalFilename, @Nullable String contentType, InputStream contentStream)throws IOException {this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream));}@Overridepublic String getName() {return this.name;}@Override@NonNullpublic String getOriginalFilename() {return this.originalFilename;}@Override@Nullablepublic String getContentType() {return this.contentType;}@Overridepublic boolean isEmpty() {return (this.content.length == 0);}@Overridepublic long getSize() {return this.content.length;}@Overridepublic byte[] getBytes() throws IOException {return this.content;}@Overridepublic InputStream getInputStream() throws IOException {return new ByteArrayInputStream(this.content);}@Overridepublic void transferTo(File dest) throws IOException {FileCopyUtils.copy(this.content, dest);}/*** File 转 MultipartFile 用完不删** @param file {@linkplain File}* @param contentType 内容类型* @return {@linkplain MultipartFile}*/public static MultipartFile fileToMultipartFile(File file, String contentType) {try {return new IMultipartFileImpl("file", file.getName(), contentType, Files.newInputStream(file.toPath()));} catch (Exception e) {throw new CommonException("File 转 MultipartFile失败!" + e.getMessage());}}
}
以上代码有后端测试断点续传的接口,本地启动项目,可以直接测试使用,所以如果前端说你有问题,怼他即可。前端实现逻辑,照着后端测试方法的思路实现即可,记录上传分片序号,计算获得上传进度。
如果白嫖过程中发现遗漏或问题的,请在评论区留言,我看到会修正或补充。
相关文章:
断点续传+测试方法完整示例
因为看不懂网上的断点续传案例,而且又不能直接复制使用,干脆自己想想写了一个。 上传入参类: import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProp…...
C#设计模式--状态模式(State Pattern)
状态模式是一种行为设计模式,它允许对象在其内部状态发生变化时改变其行为。这种模式的核心思想是将状态封装在独立的对象中,而不是将状态逻辑散布在整个程序中。 用途 简化复杂的条件逻辑:通过将不同的状态封装在不同的类中,可…...
Excel技巧:如何批量调整excel表格中的图片?
插入到excel表格中的图片大小不一,如何做到每张图片都完美的与单元格大小相同?并且能够根据单元格来改变大小?今天分享,excel表格里的图片如何批量调整大小。 方法如下: 点击表格中的一个图片,然后按住Ct…...
hadoop中导出表与数据的步骤
大家好,我是 V 哥。在Hadoop中导出表与数据,可以通过多种方式实现,包括使用Hive的EXPORT命令、MapReduce作业、Hive查询以及Sqoop工具。下面V 哥将详细介绍这些步骤和一些代码示例,来帮助大家更好理解。 1. 使用Hive的EXPORT命令…...
springBoot中的日志级别在哪里配置
在Spring Boot中,日志级别的配置可以通过多种方式来实现,主要包括在配置文件中设置、使用自定义的logback配置文件,以及在代码中动态配置等。以下是一些具体的配置方法: 一、在配置文件中设置日志级别 Spring Boot默认使用appli…...
17. Threejs案例-Three.js创建多个立方体
17. Threejs案例-Three.js创建多个立方体 实现效果 知识点 WebGLRenderer (WebGL渲染器) WebGLRenderer 是 Three.js 中用于渲染 WebGL 场景的核心类。它负责将场景中的对象渲染到画布上。 构造器 new THREE.WebGLRenderer(parameters) 参数类型描述parametersObject可选…...
数据结构——有序二叉树的删除
在上一篇博客中,我们介绍了有序二叉树的构建、遍历、查找。 数据结构——有序二叉树的构建&遍历&查找-CSDN博客文章浏览阅读707次,点赞18次,收藏6次。因为数据的类型决定数据在内存中的存储形式。left right示意为左右节点其类型也为…...
力扣1401. 圆和矩形是否有重叠
用矢量计算: class Solution { public:bool checkOverlap(int radius, int xCenter, int yCenter, int x1, int y1, int x2, int y2) {//矩形中心float Tx(float)(x1x2)/2;float Ty(float)(y1y2)/2;//强行进行对称操作,只考虑第一象限if(xCenter<Tx)…...
idea连接到docker出现 org.apache.hc.client5.http.ConnectTimeoutException 异常怎么办?
前情提要 我电脑是win11,我安装了centOS7虚拟机,配置linux环境 idea是2024社区免费版本 我就这一步步排查问题,终于发现了是因为我的2375端口没有ipv4开放,只在ipv6开放 踩坑提醒: 对了,一个一个问题排…...
一番赏小程序定制开发,打造全新抽赏体验平台
随着盲盒的热潮来袭,作为传统的潮玩方式一番赏也再次受到了大家的关注,市场热度不断上升! 一番赏能够让玩家百分百中奖,商品种类丰富、收藏价值高,拥有各种IP,从而吸引着各个圈子的粉丝玩家,用…...
PHP语法学习(第六天)
💡依照惯例,回顾一下昨天讲的内容 PHP语法学习(第五天)主要讲了PHP中的常量和运算符的运用。 🔥 想要学习更多PHP语法相关内容点击“PHP专栏” 今天给大家讲课的角色是🍍菠萝吹雪,“我菠萝吹雪吹的不是雪,而…...
按vue组件实例类型实现非侵入式国际化多语言翻译
#vue3##国际化##本地化##international# web界面国际化,I18N(Internationalization,国际化),I11L(International,英特纳雄耐尔),L10N(Localization,本地化)&…...
2024年认证杯SPSSPRO杯数学建模B题(第一阶段)神经外科手术的定位与导航解题全过程文档及程序
2024年认证杯SPSSPRO杯数学建模 B题 神经外科手术的定位与导航 原题再现: 人的大脑结构非常复杂,内部交织密布着神经和血管,所以在大脑内做手术具有非常高的精细和复杂程度。例如神经外科的肿瘤切除手术或血肿清除手术,通常需要…...
51c视觉~合集24
我自己的原文哦~ https://blog.51cto.com/whaosoft/11870494 #R-Adapter 零样本模型微调新突破,提升鲁棒性与泛化能力 论文提出新颖的Robust Adapter(R-Adapter),可以在微调零样本模型用于下游任务的同时解决这两个问题。该方…...
idea启动tomcat服务中文乱码
在idea中启动tomcat服务后部分中文乱码 但是在tomcat日志部分正常 并且在tomcat中中文也是正常 查询大量资料修改idea编码,虚拟机编码、tomcat默认编码、终端默认编码,统统没有效果。 最终发现修改tomcat下文件夹 .\conf\logging.properties 网络上…...
android studio 读写文件操作(应用场景二)
android studio版本:2023.3.1 patch2 例程:readtextviewIDsaveandread 本例程是个过渡例程,如果单是实现下图的目的有更简单的方法,但这个方法是下一步工作的基础,所以一定要做。 例程功能:将两个textvi…...
【数据结构】【线性表】特殊的线性表-字符串
目录 字符串的基本概念 字符串的三要素 字符串的基本概念 串的编码 串的实现及基本运算 顺序串的实现 串的静态数组实现 串的动态数组的实现 顺序存储的四种方案 链式串的实现 基本运算 方案三 方案一 字符串的基本概念 数据结构千千万,…...
【AWS re:Invent 2024】一文了解EKS新功能:Amazon EKS Auto Mode
文章目录 一、为什么要使用 Amazon EKS Auto Mode?二、Amazon EKS自动模式特性2.1 持续优化计算成本2.2 迁移集群操作2.3 EKS 自动模式的高级功能 三、EKS Auto 集群快速创建集群配置四、查看来自 API 服务器的指标五、EKS 相关角色权限设置六、参考链接 一、为什么…...
HTTPS的工作过程
1.HTTPS协议原理 1.1HTTPS协议的由来 HTTP在传输网络数据的时候是明文传输的,信息容易被窃听甚至篡改,因此他是一个不安全的协议(但效率高)。在如今的网络环境中,数据安全是很重要的(比如支付密码又或者各…...
Java并发编程学习之从资本家的角度看多线程和并发性(一)
目录 前言前置知识一、单线程时代二、为什么要有多线程,多线程的优点?三、使用多线程会遇到什么问题?四、多线程和并发编程的关系总结 前言 这篇文章是打开Java多线程和并发编程的大门的开始,如标题《从老板的角度看多线程和并发…...
基于STM32设计的智能宠物喂养系统(华为云IOT)_273
文章目录 一、前言1.1 项目介绍【1】项目开发背景【2】设计实现的功能【3】项目硬件模块组成【4】设计意义【5】国内外研究现状【6】摘要1.2 设计思路1.3 系统功能总结1.4 开发工具的选择【1】设备端开发【2】上位机开发1.5 参考文献1.6 系统框架图1.7 系统原理图1.8 实物图1.9…...
Mybatis-Plus的主要API
一、实体类操作相关API BaseMapper<T>接口 功能:这是 MyBatis - Plus 为每个实体类对应的 Mapper 接口提供的基础接口。它提供了一系列基本的 CRUD(增删改查)操作方法。例如insert(T entity)方法用于插入一条记录,d…...
Pillow:强大的Python图像处理库
目录 一、引言 二、Pillow 库的安装 三、Pillow 库的基本概念 四、图像的读取和保存 五、图像的基本属性 六、图像的裁剪、缩放和旋转 七、图像的颜色调整 八、图像的滤镜效果 九、图像的合成和叠加 十、图像的绘制 十一、示例程序:制作图片水印 十二、…...
Springboot定时任务详解
文章目录 Springboot定时任务详解一、引言二、cron表达式三、使用Scheduled注解1、开启定时任务2、添加定时任务 四、使用TaskScheduler接口1、注入TaskScheduler实例 五、集成Quartz框架1、集成Quartz 六、实际使用示例七、总结 Springboot定时任务详解 一、引言 在现代软件…...
【Linux】环境ChatGLM-4-9B 模型之 openai API 服务
一、摘要 最近看到 Function Call 比较感兴趣,它的核心是赋予大模型能够调用外部API的能力,能够解决大模型功能扩展性问题,允许模型调用外部数据库或API,提供特定领域的详细信息;解决信息实时性问题,模型可以实时获取最新数据;解决数据局限性问题,大模型训练数据虽多但…...
mobi文件转成pdf
将 MOBI 文件转换为 PDF 格式通常涉及两个步骤: 解析 MOBI 文件:需要提取 MOBI 文件的内容(文本、图片等)。将提取的内容转换为 PDF:将 MOBI 文件的内容渲染到 PDF 格式。 可用工具 kindleunpack 或 mobi࿱…...
Linux---对缓冲区的简单理解--第一个系统程序
前序: 首先先理解一下什么是回车与换行;回车和换行是两个概念,它们不是一个东西; 回车:光标回到开始;换行:换到下一行; 如下图: 行缓冲区 如何理解缓冲区问题? 可以认为࿰…...
word poi-tl 表格功能增强,实现表格功能垂直合并
目录 问题解决问题poi-tl介绍 功能实现引入依赖模版代码效果图 附加(插件实现)MergeColumnData 对象MergeGroupData 类ServerMergeTableData 数据信息ServerMergeTablePolicy 合并插件 问题 由于在开发功能需求中,word文档需要垂直合并表格&…...
鸿蒙实现数据管理
目录: 1、鸿蒙实现数据管理的三种方式2、用户首选项3、键值型数据管理3.1、获取KVManager实例,用于管理数据库对象3.2、创建并获取键值数据库3.3、调用put()方法向键值数据库中插入数据3.4、调用get()方法获取指定键的值3.5、调用delete()方法删除指定键…...
图片上传HTML
alioss sky:jwt:# 设置jwt签名加密时使用的秘钥admin-secret-key: itcast# 设置jwt过期时间admin-ttl: 7200000# 设置前端传递过来的令牌名称admin-token-name: tokenalioss:endpoint: ${sky.alioss.endpoint}access-key-id: ${sky.alioss.access-key-id}access-key-secret: $…...
golang 代发邮件支持附件发送,outlook案列,其他邮箱需要替换对应邮箱服务域名
GPT问答实例 import pandas as pd from openai.embeddings_utils import get_embedding, cosine_similarity import openai import os import logging as logger from flask_cors import CORS import os openai.api_key os.getenv(OPENAI_API_KEY)class Chatbot():def parse_…...
输出绝对值
输出绝对值 C语言代码C 代码Java代码Python代码 💐The Begin💐点点关注,收藏不迷路💐 输入一个浮点数,输出这个浮点数的绝对值。 输入 输入一个浮点数,其绝对值不超过10000。 输出 输出这个浮点数的绝对…...
docker desktop打包配置国内镜像地址
打包遇到无法访问外网资源,直接配置国内镜像地址 直接加入如下代码就行: {"builder": {"gc": {"defaultKeepStorage": "20GB","enabled": true}},"experimental": false,"registry-m…...
鸿蒙Next学习-监听指定页面显示/页面生命周期
自定义组件监听页面生命周期 使用无感监听页面路由的能力,能够实现在自定义组件中监听页面的生命周期。 // Index.ets import { uiObserver, router, UIObserver } from kit.ArkUI;Entry Component struct Index {listener: (info: uiObserver.RouterPageInfo) &g…...
计算机网络 —— HTTPS 协议
前一篇文章:计算机网络 —— HTTP 协议(详解)-CSDN博客 目录 前言 一、HTTPS 协议简介 二、HTTPS 工作过程 1.对称加密 2.非对称加密 3.中间人攻击 4.引入证书 三、HTTPS 常见问题 1.中间人能否篡改证书? 2.中间人能否调…...
Oracle之表空间迁移
问题背景:一个数据表随着时间的累积,导致所在表空间占用很高,里面历史数据可以清除,保留近2个月数据即可 首先通过delete删除了2个月以前的数据。 按网上的教程进行空间压缩,以下sql在表所在用户执行: -- 允许表重新…...
web组态可视化编辑器
随着工业智能制造的发展,工业企业对设备可视化、远程运维的需求日趋强烈,传统的单机版组态软件已经不能满足越来越复杂的控制需求,那么实现web组态可视化界面成为了主要的技术路径。 行业痛点 对于软件服务商来说,将单机版软件转…...
SpringMVC纯注解快速开发
此文章适合具有一定的java基础的同学看哦,如果有看不懂的基本代码还是先补补java基础哦。 此教程带您不使用xml文件而是纯注解开发,易懂、快捷、迅速,从0开始搭建,很快就能构建起一个SpringMVC项目,能学到两种使用tom…...
[读论文] Compositional 3D-aware Video Generation with LLM Director
Abstract 近年来,通过强大的生成模型和大规模互联网数据,文本到视频生成领域取得了显著进展。然而,在生成视频中精确控制单个概念(如特定角色的动作和外观、视角的移动)方面,仍存在巨大挑战。为此ÿ…...
FFmpeg 4.3 音视频-多路H265监控录放C++开发十八,ffmpeg解复用
为啥要封装和解封装呢? 1.封装就相当于将 h264 和aac 包裹在一起。既能播放声音,也能播放视频 2.在封装的时候没指定编码格式,帧率,时长,等参数;特别是视频,可以将视频帧索引存储,…...
ubuntu系统安装docker
1、 安装必要的依赖 sudo apt install apt-transport-https ca-certificates curl software-properties-common2、添加 Docker 的官方 GPG 密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -3、添加 Docker 的 APT 源 sudo add-apt-repos…...
STM32 BootLoader 刷新项目 (十三) Python上位机介绍
STM32 BootLoader 刷新项目 (十三) Python上位机介绍 大家好,这是我们STM32 BootLoader的最后一篇文章了,讲述用Python写的上位机,也更新了半年时间了,谢谢大家的支持,到目前为止,已经更新了12篇文章了&am…...
美畅物联丨智能监控,高效运维:视频汇聚平台在储能领域的实践探索
在当今全球能源格局不断变化的大背景下,对清洁能源的需求正以惊人的速度增长。储能项目作为平衡能源供需、提升能源利用效率的关键环节,其规模和复杂度也在不断攀升。在储能项目的运营管理过程中,安全监控、设备运维以及数据管理等方面面临着…...
T C P
文章目录 基于UDP应用场景 TCP协议TCP 协议段格式确认应答机制16位窗口大小 下定义32位序号和32位确认序号序号是什么?确认序号 基于UDP应用场景 UDP,tcp这样的协议根本不是直接谈UDP。tcp的应用场景,一定是上层写了应用层协议,所…...
MongoDB的简单使用
MongoDB(文档数据库)的简单使用 MongoDB最好的学习资料就是他的官方文档:SQL 到 MongoDB 的映射图表 - MongoDB 手册 v8.0 1.MongoDB CRUD操作 1.1Insert操作 基本方法: db.collection.insertOne() 将单个文档(document)插入集合中 db.collectio…...
【Exp】# Microsoft Visual C++ Redistributable 各版本下载地址
Microsoft官方页面 https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads Redistributable 2019 X86: https://aka.ms/vs/16/release/VC_redist.x86.exe X64: https://aka.ms/vs/16/release/VC_redist.x64.exe Redistributable 201…...
【MySQL】表的约束
目录 一、非空约束not null 二、默认值约束default 三、列描述comment 四、填充零zerofill 五、主键primary key 六、自增长auto_increment 七、唯一键unique 八、外键foreign key 一、非空约束not null 如果不对一个字段做非空约束,则默认为空。但空数据无…...
c++高级篇(四) ——Linux下IO多路复用之epoll模型
IO多路复用 —— epoll 前言 在之前我们就已经介绍过了select和poll,在作为io多路复用的最后一个的epoll,我们来总结一下它们之间的区别: a select 实现原理 select 通过一个文件描述符集合(fd_set)来工作,该集合可以包含需要监控的文件…...
基于Java Springboot环境保护生活App且微信小程序
一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术:Html、Css、Js、Vue、Element-ui 数据库:MySQL 后端技术:Java、Spring Boot、MyBatis 三、运行环境 开发工具:IDEA/eclipse 微信…...
.NET 9 中 LINQ 新增功能实现过程
本文介绍了.NET 9中LINQ新增功能,包括CountBy、AggregateBy和Index方法,并提供了相关代码示例和输出结果,感兴趣的朋友跟随我一起看看吧 LINQ 介绍 语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。 数据查询历来都表示为简单的…...