『博客开发日记-后台』之升级添加,修改和删除文章接口

本文最后更新于 2026年5月11日 晚上

升级添加,修改和删除文章接口


前言

由于上传文件要记录进文件表中

在写博文时会要上传 缩略图 和 文章内容的图片

所以这里也要对这些问价进行处理

另外要注意的是

在写文章的时候难免会出现一篇文章中会有多张相同的图片的情况

对于这种情况 OssFileServiceImpl 中的 moveTempToFormal 方法也要升级

删除文章时也要对在文章中提取到的图片进行去重再删除


代码实现

moveTempToFormal 方法

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
// 临时 > 正式
@Override
public UploadFileMetaVo moveTempToFormal(String tempFileUrl)
{
if (!StringUtils.hasText(tempFileUrl)) {
log.warn("临时文件的Url不存在: {}", tempFileUrl);
return null;
}

// 已经是正式地址时,直接返回,不重复转正
if (!tempFileUrl.contains(TEMP_PATH_PREFIX)) {
String filePath = extractKeyFromUrl(tempFileUrl);
return new UploadFileMetaVo(extractFileName(filePath), filePath, tempFileUrl, null, null);
}

try {
// 从URL中提取文件key
String tempKey = extractKeyFromUrl(tempFileUrl);
if (!StringUtils.hasText(tempKey)) {
log.warn("无法从URL提取临时文件key: {}", tempFileUrl);
return null;
}

// 如果临时文件已经不存在,说明可能已被前一次处理转正,直接按正式文件处理,避免重复 copy 报错
BucketManager bucketManager = getBucketManager();
FileInfo tempFileInfo;
try {
tempFileInfo = bucketManager.stat(uploadService.getBucket(), tempKey);
} catch (QiniuException e) {
log.warn("临时文件已不存在,无法继续转正: {}", tempFileUrl, e);
return null;
}

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

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

Long fileSize = tempFileInfo.fsize;
String mimeType = tempFileInfo.mimeType;

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

// 删除临时文件时间戳记录
redisCache.deleteObject(TEMP_FILE_KEY_PREFIX + tempKey);

// 返回正式文件元数据
String formalUrl = uploadService.getDomain() + formalKey;
UploadFileMetaVo uploadFileMetaVo = new UploadFileMetaVo(fileName, formalKey, formalUrl, fileSize, mimeType);
log.info("文件转正成功: {} -> {}", tempFileUrl, formalUrl);
return uploadFileMetaVo;

} catch (QiniuException e) {
log.error("文件转正失败: {}", tempFileUrl, e);
return null;
}
}

更新 三个方法

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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//添加文章/写博客
@Override
@Transactional
public ResponseResult addPosts(AddPostsDto postsDto)
{

//处理文章内容中的临时图片,将其转为正式文件
String content = postsDto.getContent();
if (ImageUrlUtils.containsTempImages(content)) {
content = processContentImagesAndSaveRecords(content, postsDto.getTitle());
postsDto.setContent(content);
}

//处理缩略图
String thumbnail = postsDto.getThumbnail();
if (StringUtils.hasText(thumbnail) && thumbnail.contains("temp/")) {
UploadFileMetaVo uploadFileMetaVo = ossFileService.moveTempToFormal(thumbnail);
//判断图片是否保存(从oss中的 临时 > 正式)成功
if (uploadFileMetaVo == null || !StringUtils.hasText(uploadFileMetaVo.getUrl())) {
throw new RuntimeException("缩略图保存失败,无法保存文章记录: " + thumbnail);
}
String fileSource = "文章《" + postsDto.getTitle() + "》中的缩略图";
thumbnail = uploadFileMetaVo.getUrl();
postsDto.setThumbnail(thumbnail);
saveFileRecord(uploadFileMetaVo.getName(),
uploadFileMetaVo.getFilePath(),
uploadFileMetaVo.getUrl(),
uploadFileMetaVo.getSize(),
uploadFileMetaVo.getMimeType(),
fileSource);
}

//添加文章
Article article = BeanCopyUtils.copyBean(postsDto, Article.class);
articleService.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();
}

//编辑/更新文章
@Override
@Transactional
public ResponseResult updatePosts(Long id, UpdatePostsDto updatePostsDto)
{
// 获取ID
updatePostsDto.setId(id);

//检查文章是否存在
Article oldArticle = articleService.getById(id);
if (oldArticle == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "该文章不存在!");
}

// 处理文章内容中的图片
String content = updatePostsDto.getContent();
List<String> oldContentImages = new ArrayList<>();
List<String> newContentImages = new ArrayList<>();

// 提取旧内容中的图片URL
if (StringUtils.hasText(oldArticle.getContent())) {
oldContentImages = ImageUrlUtils.extractImageUrls(oldArticle.getContent());
}

// 处理新内容中的临时图片
if (ImageUrlUtils.containsTempImages(content)) {
content = processContentImagesAndSaveRecords(content, oldArticle.getTitle());
updatePostsDto.setContent(content);
}

// 提取新内容中的图片URL
if (StringUtils.hasText(content)) {
newContentImages = ImageUrlUtils.extractImageUrls(content);
}

// 删除不再使用的内容图片
for (String oldImageUrl : oldContentImages) {
if (!newContentImages.contains(oldImageUrl)) {
ossFileService.deleteFileByUrl(oldImageUrl);
deleteFileRecordByUrl(oldImageUrl);
log.info("删除旧的内容图片并同步文件表: {}", oldImageUrl);
}
}

// 处理缩略图
String newThumbnail = updatePostsDto.getThumbnail();
String oldThumbnail = oldArticle.getThumbnail();

if (StringUtils.hasText(newThumbnail) && newThumbnail.contains("temp/")) {
UploadFileMetaVo uploadFileMetaVo = ossFileService.moveTempToFormal(newThumbnail);
if (uploadFileMetaVo == null || !StringUtils.hasText(uploadFileMetaVo.getUrl())) {
throw new RuntimeException("缩略图保存失败,无法更新文章记录: " + newThumbnail);
}
newThumbnail = uploadFileMetaVo.getUrl();
updatePostsDto.setThumbnail(newThumbnail);
saveFileRecord(uploadFileMetaVo.getName(),
uploadFileMetaVo.getFilePath(),
uploadFileMetaVo.getUrl(),
uploadFileMetaVo.getSize(),
uploadFileMetaVo.getMimeType(),
"文章《" + oldArticle.getTitle() + "》中的缩略图");
}

// 如果缩略图发生变化,删除旧缩略图
if (StringUtils.hasText(oldThumbnail) &&
StringUtils.hasText(newThumbnail) &&
!oldThumbnail.equals(newThumbnail)) {
ossFileService.deleteFileByUrl(oldThumbnail);
deleteFileRecordByUrl(oldThumbnail);
log.info("删除旧的缩略图并同步文件表: {}", oldThumbnail);
}

// 更新文章基本信息
Article article = BeanCopyUtils.copyBean(updatePostsDto, Article.class);
articleService.updateById(article);

// 更新标签关联关系
// 先删除旧的关联关系
LambdaQueryWrapper<ArticleTag> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(ArticleTag::getArticleId, updatePostsDto.getId());
articleTagMapper.delete(deleteWrapper);

// 添加新的关联关系
if (updatePostsDto.getTags() != null && !updatePostsDto.getTags().isEmpty()) {
List<ArticleTag> articleTags = updatePostsDto.getTags().stream()
.map(tagId -> new ArticleTag(article.getId(), tagId))
.collect(Collectors.toList());
articleTagService.saveBatch(articleTags);
}

return ResponseResult.okResult();
}

// 批量删除文章(逻辑删除),将文章中的图片有的图片从oss里的正式文件夹转移到 deleted 文件夹中
// 如果后续需要有恢复文章的功能,则可以利用逻辑删除这个特性,将文章恢复并且将图片或文件从 deleted 文件夹移动到正式文件夹中
@Override
@Transactional
public ResponseResult deletePosts(Long[] ids)
{
// 参数校验
if (ids == null || ids.length == 0) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "请选择要删除的文章");
}

List<Long> idList = List.of(ids);

//检查文章是否存在
List<Article> articlesToDelete = articleService.listByIds(idList);
if (articlesToDelete.size() != ids.length) {
//找出不存在的 文章id
Set<Long> existIds = articlesToDelete.stream()
.map(Article::getId)
.collect(Collectors.toSet());

List<Long> notExistIds = idList.stream()
.filter(id -> !existIds.contains(id))
.toList();

return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "删除失败!!原因:以下文章 id 不存在:" + notExistIds);
}

// 遍历每个文章ID,将相关图片移动到deleted文件夹,并同步删除文件表记录
for (Long articleId : idList)
{
Article article = articleService.getById(articleId);

// 查找需要移动的图片URL并去重
Set<String> imageUrlSet = new LinkedHashSet<>();
if (StringUtils.hasText(article.getContent())) {
List<String> contentImages = ImageUrlUtils.extractImageUrls(article.getContent());
imageUrlSet.addAll(contentImages);
log.info("文章ID: {} 内容中提取到的图片URL: {}", articleId, contentImages);
}
if (StringUtils.hasText(article.getThumbnail())) {
imageUrlSet.add(article.getThumbnail());
log.info("文章ID: {} 缩略图URL: {}", articleId, article.getThumbnail());
}

List<String> imageUrls = new ArrayList<>(imageUrlSet);
log.info("文章ID: {} 去重后的图片URL: {}", articleId, imageUrls);

// 批量移动图片到 deleted 文件夹
if (!imageUrls.isEmpty()) {
int movedCount = ossFileService.batchMoveFilesToDeleted(imageUrls);
log.info("文章ID: {} 的图片已移动到deleted文件夹,成功: {}/{}",
articleId, movedCount, imageUrls.size());

// 同步删除文件表记录
for (String imageUrl : imageUrls) {
deleteFileRecordByUrl(imageUrl);
}
log.info("文章ID: {} 的图片文件表记录已删除,共 {} 条", articleId, imageUrls.size());
}

// 删除文章标签关联关系
LambdaQueryWrapper<ArticleTag> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(ArticleTag::getArticleId, articleId);
int deletedCount = articleTagMapper.delete(deleteWrapper);
log.info("删除文章ID: {} 的标签关联记录,共 {} 条", articleId, deletedCount);
}

// 逻辑删除文章(将 del_flag 从 0 > 1)
articleService.removeByIds(idList);
log.info("批量逻辑删除文章成功,ID列表: {}", idList);

return ResponseResult.okResult();
}



//处理文章内容中的图片并将其写入文件表
private String processContentImagesAndSaveRecords(String content, String postTitle)
{
List<String> tempImageUrls = ImageUrlUtils.extractTempImageUrls(content);
String result = content;

// 因为同一篇文章里可能重复引用同一张临时图片,所一这里先去重,避免重复转正和重复删除临时文件
List<String> distinctTempImageUrls = tempImageUrls.stream().distinct().collect(Collectors.toList());

for (String tempUrl : distinctTempImageUrls) {
UploadFileMetaVo uploadFileMetaVo = ossFileService.moveTempToFormal(tempUrl);

//判断图片是否保存(从oss中的 临时 > 正式)成功
if (uploadFileMetaVo == null || !StringUtils.hasText(uploadFileMetaVo.getUrl())) {
throw new RuntimeException("图片保存失败,无法保存文章内容中的图片记录: " + tempUrl);
}

result = ImageUrlUtils.replaceImageUrl(result, tempUrl, uploadFileMetaVo.getUrl());
if (StringUtils.hasText(uploadFileMetaVo.getFilePath())) {
String fileSource = "文章《" + postTitle + "》中的图片";
saveFileRecord(uploadFileMetaVo.getName(),
uploadFileMetaVo.getFilePath(),
uploadFileMetaVo.getUrl(),
uploadFileMetaVo.getSize(),
uploadFileMetaVo.getMimeType(),
fileSource);
}
}

return result;
}

//将元数据写入文件表
private void saveFileRecord(String fileName, String filePath, String url, Long size, String mimeType, String fileSource)
{
SysFile sysFile = new SysFile();
sysFile.setName(fileName);
sysFile.setFilePath(filePath);
sysFile.setUrl(url);
sysFile.setSize(size);
sysFile.setMimeType(mimeType);
sysFile.setFileSource(fileSource);
sysFile.setType(SystemConstants.FILE_TYPE_IMAGE);
sysFileService.save(sysFile);
}

//处理删除文件
private void deleteFileRecordByUrl(String url)
{
if (!StringUtils.hasText(url)) {
return;
}

LambdaQueryWrapper<SysFile> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysFile::getUrl, url);
sysFileService.remove(queryWrapper);
}





PS:该系列只做为作者学习开发项目做的笔记用

不一定符合读者来学习,仅供参考


预告

后续会记录博客的开发过程

每次学习会做一份笔记来进行发表

“一花一世界,一叶一菩提”


版权所有 © 2026 云梦泽
欢迎访问我的个人网站:https://hgt12.github.io/


『博客开发日记-后台』之升级添加,修改和删除文章接口
http://example.com/2026/05/10/『博客开发日记-后台』之升级添加,修改和删除文章接口/
作者
云梦泽
发布于
2026年5月10日
更新于
2026年5月11日
许可协议