『博客开发日记』之对oss文件存储服务进行升级

本文最后更新于 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
/**
* OSS文件管理服务接口
*
* @author mengze
*/
public interface OssFileService
{
/**
* 将临时文件转为正式文件
* @param tempFileUrl 临时文件URL
* @return 正式文件URL
*/
String moveTempToFormal(String tempFileUrl);

/**
* 批量将临时文件转为正式文件
* @param tempFileUrls 临时文件URL列表
* @return 正式文件URL列表
*/
List<String> batchMoveTempToFormal(List<String> tempFileUrls);

/**
* 删除OSS上的文件
* @param fileKey 文件key(路径)
* @return 是否删除成功
*/
boolean deleteFile(String fileKey);

/**
* 根据完整URL删除OSS上的文件
* @param fileUrl 文件完整URL
* @return 是否删除成功
*/
boolean deleteFileByUrl(String fileUrl);

/**
* 处理内容中的临时图片,将其转为正式文件
* @param content 包含图片URL的内容(HTML、Markdown等)
* @return 处理后的内容
*/
String processContentImages(String content);

/**
* 清理过期的临时文件
* @return 清理的文件数量
*/
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
/**
* OSS文件管理服务实现类
*
* @author mengze
*/
@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/";

/**
* 获取BucketManager实例
*/
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 {
// 从URL中提取文件key
String tempKey = extractKeyFromUrl(tempFileUrl);

// 生成正式文件的key(去掉temp/前缀)
String formalKey = tempKey.replace(TEMP_PATH_PREFIX, "");

// 在OSS上移动文件(实际是复制+删除)
BucketManager bucketManager = getBucketManager();
bucketManager.copy(ossUploadService.getBucket(), tempKey, ossUploadService.getBucket(), formalKey, true);

// 删除临时文件
bucketManager.delete(ossUploadService.getBucket(), tempKey);

// 从Redis中删除临时文件记录
redisCache.deleteObject(TEMP_FILE_KEY_PREFIX + tempKey);

// 返回正式文件URL
String formalUrl = ossUploadService.getDomain() + formalKey;
log.info("文件转正成功: {} -> {}", tempFileUrl, formalUrl);
return formalUrl;

} catch (QiniuException e) {
log.error("文件转正失败: {}", tempFileUrl, e);
return tempFileUrl; // 失败时返回原URL
}
}

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

// 从URL中提取文件key
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;
}

// 提取所有临时图片URL
List<String> tempImageUrls = ImageUrlUtils.extractTempImageUrls(content);

// 逐个转换为正式URL
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 {
// 从Redis获取所有临时文件记录
Collection<String> keys = redisCache.keys(TEMP_FILE_KEY_PREFIX + "*");

if (keys == null || keys.isEmpty()) {
log.info("没有需要清理的临时文件");
return 0;
}

long currentTime = System.currentTimeMillis();
//long expireTime = 60 * 1000; // 测试1分钟触发一次
long expireTime = 24 * 60 * 60 * 1000; // 24小时

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

// 如果文件已过期(超过24小时)
if (uploadTime != null && (currentTime - uploadTime) > expireTime) {
// 提取文件key
String fileKey = redisKey.replace(TEMP_FILE_KEY_PREFIX, "");

// 删除OSS上的文件
if (deleteFile(fileKey)) {
// 删除Redis记录
redisCache.deleteObject(redisKey);
cleanedCount++;
log.info("清理过期临时文件: {}", fileKey);
}
}
}

log.info("临时文件清理完成,共清理 {} 个文件", cleanedCount);

} catch (Exception e) {
log.error("清理临时文件时发生错误", e);
}

return cleanedCount;
}

/**
* 从URL中提取文件key
*/
private String extractKeyFromUrl(String url)
{
if (!StringUtils.hasText(url)) {
return "";
}

// 如果URL包含域名,去掉域名部分
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
/**
* 图片URL处理工具类
* 用于处理文章内容和富文本中的图片URL
*
* @author mengze
*/
public class ImageUrlUtils
{
// 匹配临时图片URL的正则表达式
private static final String TEMP_IMAGE_REGEX = "(https?://[^\\s\"']+/temp/[^\\s\"']+)";
private static final Pattern TEMP_IMAGE_PATTERN = Pattern.compile(TEMP_IMAGE_REGEX);

// 匹配所有图片URL的正则表达式
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);

/**
* 从内容中提取所有图片URL
*
* @param content 富文本内容(HTML、Markdown等)
* @return 图片URL列表
*/
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;
}

/**
* 从内容中提取所有临时图片URL
*
* @param content 富文本内容
* @return 临时图片URL列表
*/
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;
}

/**
* 检查内容中是否包含临时图片
*
* @param content 富文本内容
* @return 是否包含临时图片
*/
public static boolean containsTempImages(String content)
{
if (!StringUtils.hasText(content)) {
return false;
}

return content.contains("temp/");
}

/**
* 替换内容中的图片URL
*
* @param content 原始内容
* @param oldUrl 旧的图片URL
* @param newUrl 新的图片URL
* @return 替换后的内容
*/
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
//Oss上传图片实现类
@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class OssUploadServiceImpl implements UploadService
{
private String accessKey;
private String secretKey;
private String bucket;
private String domain; // OSS访问域名

@Autowired
private RedisCache redisCache;

// Redis key前缀,用于存储临时文件信息
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);
}

//判断文件大小(限制2MB)
long maxSize = 2 * 1024 * 1024; // 2MB
if (img.getSize() > maxSize) {
throw new SystemException(AppHttpCodeEnum.FILE_SIZE_ERROR);
}

//判断文件类型
//获取原始文件名
String originalFilename = img.getOriginalFilename();
//判断文件名是否为png,jpg,gif,jpeg
if (!originalFilename.endsWith(".png") && !originalFilename.endsWith(".jpg") && !originalFilename.endsWith(".gif") && !originalFilename.endsWith(".jpeg")) {
throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
}

//上传到临时目录,文件路径格式:temp/yyyy/MM/dd/文件名.后缀
String filePath = "temp/" + PathUtils.generateFilePath(originalFilename);
String url = uploadOss(img, filePath);

//将临时文件信息存入Redis,设置24小时过期时间
//存储格式:key=temp:file:文件路径, value=上传时间戳
redisCache.setCacheObject(TEMP_FILE_KEY_PREFIX + filePath, System.currentTimeMillis(), 24, TimeUnit.HOURS);

return ResponseResult.okResult(url);
}


private String uploadOss(MultipartFile imgFile, String filePath)
{
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//默认不指定key的情况下,以文件内容的hash值作为文件名
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);
//返回图片的完整URL
return domain + key;
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
} catch (Exception ex) {
//ignore
}
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
/**
* 清理临时文件定时任务
* 每天凌晨2点执行,清理超过24小时的临时文件
*
* @author mengze
*/
@Component
@Slf4j
public class CleanTempFilesJob
{
@Autowired
private OssFileService ossFileService;

/**
* 每天凌晨2点执行清理任务
* cron表达式:秒 分 时 日 月 周
*/
//@Scheduled(cron = "0 * * * * ?") //测试1分钟执行一次
@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);
}

//如果头像发生变化,删除OSS上的旧头像
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条件更新
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)
{
// 如果有ID,说明是更新操作,抛出异常
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);

//初始化Redis中的浏览量为0
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 云梦泽
欢迎访问我的个人网站:


『博客开发日记』之对oss文件存储服务进行升级
http://example.com/2026/04/13/『博客开发日记』之对oss文件存储服务进行升级/
作者
云梦泽
发布于
2026年4月13日
更新于
2026年4月13日
许可协议