断点续传+测试方法完整示例
因为看不懂网上的断点续传案例,而且又不能直接复制使用,干脆自己想想写了一个。
上传入参类:
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# 中的静态构造函数和实例构造函数的区别
在C#中,静态构造函数和实例构造函数在类的初始化过程中扮演着不同的角色。下面我将详细介绍这两种构造函数的区别: 实例构造函数(Instance Constructor): 实例构造函数用于初始化类的实例(对象)…...

如何在UI自动化测试中创建稳定的定位器?
如何在UI自动化测试中创建稳定的定位器? 前言1. 避免使用绝对路径2. 避免在定位器中使用索引3. 避免多个类名的定位器4. 避免动态和自动生成的ID5. 确保定位器唯一6. 处理隐藏元素的策略7. 谨慎使用基于文本的定位器8. 使用AI创建稳定的定位器 总结 前言 在自动化测…...

【5G】5G技术组件 5G Technology Components
5G的目标设置非常高,不仅在数据速率上要求达到20Gbps,在容量提升上要达到1000倍,还要为诸如大规模物联网(IoT, Internet of Things)和关键通信等新服务提供灵活的平台。这些高目标要求5G网络采用多种新技术…...
四十一:Web传递消息时的编码格式
在现代Web应用中,数据在客户端和服务器之间的传递往往需要经过特定的编码方式。不同类型的数据(如文本、图像、文件等)需要用不同的编码格式进行表示,以确保信息的准确性与安全性。本文将介绍Web传递消息时常用的几种编码格式&…...

【细如狗】记录一次使用MySQL的Binlog进行数据回滚的完整流程
文章目录 1 事情起因2 解决思路3 利用binlog进行数据回滚 3.1 确认是否启用Binlog日志3.2 确认是否有binlog文件3.3 找到误操作的时间范围3.4 登录MySQL服务器查找binlog文件 3.4.1 查询binlog文件路径3.4.2 找到binlog文件3.4.3 确认误操作被存储在哪一份binlog文件中 3.5 查…...
什么是云原生数据库 PolarDB?
云原生数据库 PolarDB 是阿里云推出的一款高性能、兼容性强、弹性灵活的关系型数据库产品。它基于云原生架构设计,结合分布式存储和计算分离的技术优势,为用户提供强大的计算能力、卓越的可靠性以及高性价比的数据库解决方案。PolarDB 适合各种业务场景&…...

Kafka Stream实战教程
Kafka Stream实战教程 1. Kafka Streams 基础入门 1.1 什么是 Kafka Streams Kafka Streams 是 Kafka 生态中用于 处理实时流数据 的一款轻量级流处理库。它利用 Kafka 作为数据来源和数据输出,可以让开发者轻松地对实时数据进行处理,比如计数、聚合、…...

BEPUphysicsint定点数3D物理引擎使用
原文:BEPUphysicsint定点数3D物理引擎使用 - 哔哩哔哩 上一节給大家介绍了BEPUphysicsint的一些基本的情况,这节课我们来介绍它的基本使用,本节主要从以下5个方面来介绍: (1) 创建一个物理世界Space,并开启模拟迭代; (2) 添加一个物理物体…...
Splatter Image运行笔记
文章标题:Splatter Image: Ultra-Fast Single-View 3D Reconstruction 1. 环境配置 下载Splatter Image代码 git clone https://github.com/szymanowiczs/splatter-image.git 创建环境 conda create --name splatter-image python3.8 激活环境 conda activat…...

python爬虫--某房源网站验证码破解
文章目录 使用模块爬取目标验证码技术细节实现成果代码实现使用模块 requests请求模块 lxml数据解析模块 ddddocr光学识别 爬取目标 网站验证码破解思路是统一的,本文以城市列表为例 目标获取城市名以及城市连接,之后获取城市房源信息技术直接替换地址即可 验证码 技术…...
Micropython编译ESP32C3开发板版本过程详细步骤步骤
一、环境说明 开发板:合宙ESP32-C3 工作机器CPU:AMD64 操作系统:Windows10 2004(19041.508) 使用WSL2安装Linux系统 Linux:Ubuntu 24.04.1 LTS python:python 3.12.3(Windows和…...

【开源免费】基于SpringBoot+Vue.JS大创管理系统(JAVA毕业设计)
博主说明:本文项目编号 T 081 ,文末自助获取源码 \color{red}{T081,文末自助获取源码} T081,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…...
mysql 和 tidb的区别
MySQL 和 TiDB 是两种常见的关系型数据库管理系统,但它们的设计理念和适用场景有显著区别。以下从架构、性能、扩展性、适用场景等方面进行对比: 架构设计 MySQL 单机架构为主,可通过主从复制实现读写分离或高可用。分布式支持依赖外部组件&…...

传输层5——TCP可靠传输的实现(重点!!)
TCP协议如何实现可靠传输?确保接收方收到数据? 需要依靠几个结构: 以字节为单位的滑动窗口 这其中包括发送方的发送窗口和接收方的接收窗口 下面的描述,我们指定A为发送端口,B为接收端口 TCP的可靠传输就是靠着滑动窗口…...

基于Python实现web网页内容爬取
文章目录 1. 网页分析2. 获取网页信息2.1 使用默认的urllib.request库2.2 使用requests库1.3 urllib.request 和 requests库区别 2. 更改用户代理3. BeautifulSoup库筛选数据3.1 soup.find()和soup.find_all() 函数 4. 抓取分页链接参考资料 在日常学习和工作中,我们…...

Centos7和9安装mysql5.7和mysql8.0详细教程(超详细)
目录 一、简介 1.1 什么是数据库 1.2 什么是数据库管理系统(DBMS) 1.3 数据库的作用 二、安装MySQL 1.1 国内yum源安装MySQL5.7(centos7) (1)安装4个软件包 (2)找到4个软件包…...

星闪WS63E开发板的OpenHarmony环境构建
目录 引言 关于SDK 安装步骤 1. 更新并安装基本依赖 2. 设置 Python 3.8 为默认版本 3. 安装 Python 依赖 4. 安装有冲突的包 5. 设置工作目录 6. 设置环境变量 7. 下载预构建文件以及安装编译工具 8. 编译工程 nearlink_dk_3863 设置编译产品 编译 制品存放路径…...
MongoDB数据建模小案例
MongoDB数据建模小案例 朋友圈评论内容管理 需求 社交类的APP需求,一般都会引入“朋友圈”功能,这个产品特性有一个非常重要的功能就是评论体系。 先整理下需求: 这个APP希望点赞和评论信息都要包含头像信息: 点赞列表,点赞用户的昵称,头像;评论列表,评论用户的昵称…...

MySQL(库的操作)
目录 1. 创建数据库 2. 删除数据库 3. 查看数据库 4. 修改数据库 5. 备份和恢复 6. 查看连接情况 1. 创建数据库 CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [, create_specification] ...] 1. 大写的是关键字 2. [ ]可带可不带 3. db_name 数据…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...

浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求,本次涉及的主要是收费汇聚交换机的配置,浪潮网络设备在高速项目很少,通…...