本文最后更新于 2026年4月13日 晚上
对oss文件存储服务进行升级
前言
在之前的开发中有对上传图片等文件进行处理
将图片上传到oss对象存储中
但是存在一个问题是
当用户调用上传接口时会上传图像到oss
这样并没有什么问题
但是用户上传但是不保存时图片还是上传上去了
举个例子就是
用户上传头像 > 成功上传至oss > 不保存 > 图片仍然保存在oss
由于oss对象存储空间是有限的
如果对上传的图片不加限制
长此以往空间就会被大量无用的文件占满 (如果有人恶意上传这个进度就会更快)
所以要对之前的oss上传文件服务类进行升级
还有一个使用场景就是
用户已经上传并保存了文件
但是想再更新对该文件进行更新的话就会触发一下流程
上传并保存文件 > 成功上传至oss > 用户更新文件 > 新文件上传oss > 原旧文件仍保存在oss中
这样也会导致大量文件无效文件保存你再oss中
需求
在用户上传图片时先将图片上传至存储空间的 temp (临时)文件夹
通过 redis 缓存保存临时文件 即将临时文件信息存入Redis
通过设置24小时过期的时间 定时清理 redis 里的缓存文件
如果用户选择确定保存文件
则将文件的临时属性转换成永久保存属性(使用文件的基础复制转移 + 删除原临时文件实现)
当用户更新文件时
找到旧文件并将新文件和旧文件进行替换
代码实现
先创建 OssFileService 和 OssFileServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
public interface OssFileService {
String moveTempToFormal(String tempFileUrl);
List<String> batchMoveTempToFormal(List<String> tempFileUrls);
boolean deleteFile(String fileKey);
boolean deleteFileByUrl(String fileUrl);
String processContentImages(String content);
int cleanExpiredTempFiles(); }
|
OSS文件管理服务 OssFileServiceImpl 中主要有下面几个功能
将临时文件转正式文件
删除文件
对写文章内容里的图片进行处理
清理过期文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
|
@Service @Slf4j public class OssFileServiceImpl implements OssFileService { @Autowired private OssUploadServiceImpl ossUploadService; @Autowired private RedisCache redisCache; private static final String TEMP_FILE_KEY_PREFIX = "temp:file:"; private static final String TEMP_PATH_PREFIX = "temp/";
private BucketManager getBucketManager() { Configuration cfg = new Configuration(Region.autoRegion()); Auth auth = Auth.create(ossUploadService.getAccessKey(), ossUploadService.getSecretKey()); return new BucketManager(auth, cfg); } @Override public String moveTempToFormal(String tempFileUrl) { if (!StringUtils.hasText(tempFileUrl) || !tempFileUrl.contains(TEMP_PATH_PREFIX)) { return tempFileUrl; } try { String tempKey = extractKeyFromUrl(tempFileUrl); String formalKey = tempKey.replace(TEMP_PATH_PREFIX, ""); BucketManager bucketManager = getBucketManager(); bucketManager.copy(ossUploadService.getBucket(), tempKey, ossUploadService.getBucket(), formalKey, true); bucketManager.delete(ossUploadService.getBucket(), tempKey); redisCache.deleteObject(TEMP_FILE_KEY_PREFIX + tempKey); String formalUrl = ossUploadService.getDomain() + formalKey; log.info("文件转正成功: {} -> {}", tempFileUrl, formalUrl); return formalUrl; } catch (QiniuException e) { log.error("文件转正失败: {}", tempFileUrl, e); return tempFileUrl; } } @Override public List<String> batchMoveTempToFormal(List<String> tempFileUrls) { if (tempFileUrls == null || tempFileUrls.isEmpty()) { return new ArrayList<>(); } return tempFileUrls.stream() .map(this::moveTempToFormal) .collect(Collectors.toList()); } @Override public boolean deleteFile(String fileKey) { try { BucketManager bucketManager = getBucketManager(); bucketManager.delete(ossUploadService.getBucket(), fileKey); log.info("从oss中删除文件成功: {}", fileKey); return true; } catch (QiniuException e) { log.error("从oss中删除文件失败: {}", fileKey, e); return false; } } @Override public boolean deleteFileByUrl(String fileUrl) { if (!StringUtils.hasText(fileUrl)) { return false; } String fileKey = extractKeyFromUrl(fileUrl); if (!StringUtils.hasText(fileKey)) { log.warn("无法从URL提取文件key: {}", fileUrl); return false; } return deleteFile(fileKey); } @Override public String processContentImages(String content) { if (!ImageUrlUtils.containsTempImages(content)) { return content; } List<String> tempImageUrls = ImageUrlUtils.extractTempImageUrls(content); String result = content; for (String tempUrl : tempImageUrls) { String formalUrl = moveTempToFormal(tempUrl); result = ImageUrlUtils.replaceImageUrl(result, tempUrl, formalUrl); } return result; } @Override public int cleanExpiredTempFiles() { int cleanedCount = 0; try { Collection<String> keys = redisCache.keys(TEMP_FILE_KEY_PREFIX + "*"); if (keys == null || keys.isEmpty()) { log.info("没有需要清理的临时文件"); return 0; } long currentTime = System.currentTimeMillis(); long expireTime = 24 * 60 * 60 * 1000; for (String redisKey : keys) { Object uploadTimeObj = redisCache.getCacheObject(redisKey); Long uploadTime = null; if (uploadTimeObj instanceof Long) { uploadTime = (Long) uploadTimeObj; } else if (uploadTimeObj instanceof String) { try { uploadTime = Long.parseLong((String) uploadTimeObj); } catch (NumberFormatException e) { log.warn("无法解析上传时间: {}, 值: {}", redisKey, uploadTimeObj); continue; } } else if (uploadTimeObj instanceof Number) { uploadTime = ((Number) uploadTimeObj).longValue(); } if (uploadTime != null && (currentTime - uploadTime) > expireTime) { String fileKey = redisKey.replace(TEMP_FILE_KEY_PREFIX, ""); if (deleteFile(fileKey)) { redisCache.deleteObject(redisKey); cleanedCount++; log.info("清理过期临时文件: {}", fileKey); } } } log.info("临时文件清理完成,共清理 {} 个文件", cleanedCount); } catch (Exception e) { log.error("清理临时文件时发生错误", e); } return cleanedCount; }
private String extractKeyFromUrl(String url) { if (!StringUtils.hasText(url)) { return ""; } if (url.contains("://")) { int index = url.indexOf("/", url.indexOf("://") + 3); if (index > 0) { return url.substring(index + 1); } } return url; } }
|
对于后台写博客时对上传图片的处理
创建一个 ImageUrlUtils 工具类给 OssFileServiceImpl 中需要的地方调用
用于处理文章内容和富文本中的图片URL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
public class ImageUrlUtils { private static final String TEMP_IMAGE_REGEX = "(https?://[^\\s\"']+/temp/[^\\s\"']+)"; private static final Pattern TEMP_IMAGE_PATTERN = Pattern.compile(TEMP_IMAGE_REGEX); private static final String IMAGE_URL_REGEX = "(https?://[^\\s\"']+\\.(jpg|jpeg|png|gif|webp|bmp))"; private static final Pattern IMAGE_URL_PATTERN = Pattern.compile(IMAGE_URL_REGEX, Pattern.CASE_INSENSITIVE);
public static List<String> extractImageUrls(String content) { List<String> imageUrls = new ArrayList<>(); if (!StringUtils.hasText(content)) { return imageUrls; } Matcher matcher = IMAGE_URL_PATTERN.matcher(content); while (matcher.find()) { String imageUrl = matcher.group(1); imageUrls.add(imageUrl); } return imageUrls; }
public static List<String> extractTempImageUrls(String content) { List<String> tempImageUrls = new ArrayList<>(); if (!StringUtils.hasText(content)) { return tempImageUrls; } Matcher matcher = TEMP_IMAGE_PATTERN.matcher(content); while (matcher.find()) { String tempUrl = matcher.group(1); tempImageUrls.add(tempUrl); } return tempImageUrls; }
public static boolean containsTempImages(String content) { if (!StringUtils.hasText(content)) { return false; } return content.contains("temp/"); }
public static String replaceImageUrl(String content, String oldUrl, String newUrl) { if (!StringUtils.hasText(content) || !StringUtils.hasText(oldUrl)) { return content; } return content.replace(oldUrl, newUrl); } }
|
更新 OssUploadServiceImpl 实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| @Service @Data @ConfigurationProperties(prefix = "oss") public class OssUploadServiceImpl implements UploadService { private String accessKey; private String secretKey; private String bucket; private String domain; @Autowired private RedisCache redisCache; private static final String TEMP_FILE_KEY_PREFIX = "temp:file:";
@Override public ResponseResult uploadImg(MultipartFile img) { if (img == null || img.isEmpty()) { throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR); } long maxSize = 2 * 1024 * 1024; if (img.getSize() > maxSize) { throw new SystemException(AppHttpCodeEnum.FILE_SIZE_ERROR); } String originalFilename = img.getOriginalFilename(); if (!originalFilename.endsWith(".png") && !originalFilename.endsWith(".jpg") && !originalFilename.endsWith(".gif") && !originalFilename.endsWith(".jpeg")) { throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR); } String filePath = "temp/" + PathUtils.generateFilePath(originalFilename); String url = uploadOss(img, filePath); redisCache.setCacheObject(TEMP_FILE_KEY_PREFIX + filePath, System.currentTimeMillis(), 24, TimeUnit.HOURS); return ResponseResult.okResult(url); }
private String uploadOss(MultipartFile imgFile, String filePath) { Configuration cfg = new Configuration(Region.autoRegion()); cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2; UploadManager uploadManager = new UploadManager(cfg); String key = filePath; try { InputStream inputStream = imgFile.getInputStream(); Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(inputStream,key,upToken,null, null); DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); return domain + key; } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { } } } catch (Exception ex) { } return key; } }
|
编写一个定时工作类 CleanTempFilesJob 用于执行清理临时文件定时任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
@Component @Slf4j public class CleanTempFilesJob { @Autowired private OssFileService ossFileService;
@Scheduled(cron = "0 0 2 * * ?") public void cleanExpiredTempFiles() { log.info("开始执行临时文件清理任务"); try { int cleanedCount = ossFileService.cleanExpiredTempFiles(); log.info("临时文件清理任务完成,共清理 {} 个文件", cleanedCount); } catch (Exception e) { log.error("临时文件清理任务执行失败", e); } } }
|
然后就是对之前一些接口的实现方法进行升级(前台更新用户信息,后台添加文章)
更新 SysUserServiceImpl 中的 updateUserInfo 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| @Override public ResponseResult updateUserInfo(SysUser sysUser) { Long currentUserId = SecurityUtils.getUserId(); if (currentUserId == null) { return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN); }
if (!StringUtils.hasText(sysUser.getNickname())) { return ResponseResult.errorResult(AppHttpCodeEnum.NICKNAME_NOT_NULL, "昵称不能为空"); }
if (!StringUtils.hasText(sysUser.getEmail()) && !StringUtils.hasText(sysUser.getPhone())) { return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "邮箱和电话号码不能同时为空"); }
if (StringUtils.hasText(sysUser.getPhone()) && !sysUser.getPhone().matches(PHONE_REGEX)) { return ResponseResult.errorResult(AppHttpCodeEnum.PHONE_FORMAT_ERROR, "请输入正确的手机号码"); }
if (StringUtils.hasText(sysUser.getPhone())) { LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getPhone, sysUser.getPhone()) .ne(SysUser::getId, currentUserId); SysUser existSysUser = getOne(queryWrapper); if (existSysUser != null) { return ResponseResult.errorResult(AppHttpCodeEnum.PHONE_OCCUPIED, "你用别人的电话号码干嘛?"); } }
if (StringUtils.hasText(sysUser.getEmail())) { LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getEmail, sysUser.getEmail()) .ne(SysUser::getId, currentUserId); SysUser existSysUser = getOne(queryWrapper); if (existSysUser != null) { return ResponseResult.errorResult(AppHttpCodeEnum.EMAIL_OCCUPIED, "你用别人的邮箱干嘛?"); } }
if (StringUtils.hasText(sysUser.getNickname())) { LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getNickname, sysUser.getNickname()) .ne(SysUser::getId, currentUserId); SysUser existSysUser = getOne(queryWrapper); if (existSysUser != null) { return ResponseResult.errorResult(AppHttpCodeEnum.NICKNAME_OCCUPIED, "这个昵称和别人的重复啦!"); } }
SysUser oldSysUser = getById(currentUserId); String avatar = sysUser.getAvatar(); if (StringUtils.hasText(avatar) && avatar.contains("temp/")) { avatar = ossFileService.moveTempToFormal(avatar); sysUser.setAvatar(avatar); } String oldAvatar = oldSysUser.getAvatar(); if (StringUtils.hasText(avatar) && StringUtils.hasText(oldAvatar) && !avatar.equals(oldAvatar)) { try { ossFileService.deleteFileByUrl(oldAvatar); log.info("从oss中删除旧头像成功: {}", oldAvatar); } catch (Exception e) { log.warn("从oss中删除旧头像失败: {}, 错误: {}", oldAvatar, e.getMessage()); } } LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<SysUser>() .eq(SysUser::getId, currentUserId) .set(SysUser::getNickname, sysUser.getNickname()) .set(SysUser::getSex, sysUser.getSex()) .set(SysUser::getPhone, StringUtils.hasText(sysUser.getPhone()) ? sysUser.getPhone() : null) .set(SysUser::getEmail, StringUtils.hasText(sysUser.getEmail()) ? sysUser.getEmail() : null) .set(SysUser::getAvatar, avatar);
boolean success = update(null, wrapper); if (!success) { return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "更新用户信息失败"); } boolean avatarChanged = !avatar.equals(oldSysUser.getAvatar()); boolean nicknameChanged = !sysUser.getNickname().equals(oldSysUser.getNickname()); if (avatarChanged || nicknameChanged) { LambdaUpdateWrapper<com.mengze.domain.entity.Comment> commentWrapper = new LambdaUpdateWrapper<com.mengze.domain.entity.Comment>() .eq(com.mengze.domain.entity.Comment::getUserId, currentUserId); if (avatarChanged) { commentWrapper.set(com.mengze.domain.entity.Comment::getAvatar, avatar); } if (nicknameChanged) { commentWrapper.set(com.mengze.domain.entity.Comment::getNickname, sysUser.getNickname()); } commentMapper.update(null, commentWrapper); } return ResponseResult.okResult(); }
|
更新 AdminPostsServiceImpl 中的 addPosts 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Override @Transactional public ResponseResult addPosts(AddPostsDto postsDto) { if (postsDto.getId() != null) { throw new RuntimeException("新增文章时不应该包含ID"); }
String content = postsDto.getContent(); if (ImageUrlUtils.containsTempImages(content)) { content = ossFileService.processContentImages(content); postsDto.setContent(content); }
String thumbnail = postsDto.getThumbnail(); if (StringUtils.hasText(thumbnail) && thumbnail.contains("temp/")) { thumbnail = ossFileService.moveTempToFormal(thumbnail); postsDto.setThumbnail(thumbnail); }
Article article = BeanCopyUtils.copyBean(postsDto, Article.class); save(article);
redisCache.setCacheMapValue(SystemConstants.ARTICLE_VIEW_COUNT, article.getId().toString(), 0);
List<ArticleTag> articleTags = postsDto.getTags().stream() .map(tagId -> new ArticleTag(article.getId(), tagId)) .collect(Collectors.toList());
articleTagService.saveBatch(articleTags); return ResponseResult.okResult(); }
|
至此对oss文件存储服务的升级完成
PS:该系列只做为作者学习开发项目做的笔记用
不一定符合读者来学习,仅供参考
预告
后续会记录博客的开发过程
每次学习会做一份笔记来进行发表
“一花一世界,一叶一菩提”
版权所有 © 2026 云梦泽
欢迎访问我的个人网站: