『博客开发日记』之修改前台上传接口的方法

本文最后更新于 2026年5月8日 下午

修改前台上传接口的方法


前言

为了适配后台的文件管理模块

数据库中有文件表

前台在修改用户信息的时候(尤其是上传头像操作)

要将用户上传的头像记录进文件表

这样方便管理

再将后台写博文中上传图片的接口使用公用的上传图片接口


代码实现

首先创建 UploadFileMetaVo 用于存储上传图片的元数据 方便与oss沟通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 上传文件的元数据
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UploadFileMetaVo {

@ApiModelProperty(value = "文件名", example = "fd2c11e91a984e80a80004a22ca6707d_avatar.png")
private String name;

@ApiModelProperty(value = "文件路径", example = "2026/05/07/fd2c11e91a984e80a80004a22ca6707d_avatar.png")
private String filePath;

@ApiModelProperty(value = "文件URL", example = "http://t8ul2nlc8.hn-bkt.clouddn.com/2026/05/07/fd2c11e91a984e80a80004a22ca6707d_avatar.png")
private String url;

@ApiModelProperty(value = "文件大小,单位字节", example = "102400")
private Long size;

@ApiModelProperty(value = "MIME类型", example = "image/png")
private String mimeType;
}


更新 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
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
/**
* OSS文件管理服务实现类
*
* @author mengze
*/
@Service
@Slf4j
public class OssFileServiceImpl implements OssFileService
{
@Autowired
private UploadServiceImpl uploadService;

@Autowired
private RedisCache redisCache;

private static final String TEMP_FILE_KEY_PREFIX = "temp:file:";
private static final String TEMP_PATH_PREFIX = "temp/";
private static final String DELETED_PATH_PREFIX = "deleted/";

/**
* 获取BucketManager实例
*/
private BucketManager getBucketManager()
{
Configuration cfg = new Configuration(Region.autoRegion());
Auth auth = Auth.create(uploadService.getAccessKey(), uploadService.getSecretKey());
return new BucketManager(auth, cfg);
}

// 临时 > 正式
@Override
public UploadFileMetaVo moveTempToFormal(String tempFileUrl)
{
if (!StringUtils.hasText(tempFileUrl) || !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);

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

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

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

// 返回正式文件元数据
String formalUrl = uploadService.getDomain() + formalKey;
Long fileSize = null;
String mimeType = null;
Object tempMetaObj = redisCache.getCacheObject(TEMP_FILE_KEY_PREFIX + tempKey + ":meta");
if (tempMetaObj instanceof Map) {
Map<?, ?> tempMeta = (Map<?, ?>) tempMetaObj;
Object sizeObj = tempMeta.get("size");
Object mimeTypeObj = tempMeta.get("mimeType");
if (sizeObj instanceof Number) {
fileSize = ((Number) sizeObj).longValue();
} else if (sizeObj instanceof String && StringUtils.hasText((String) sizeObj)) {
try {
fileSize = Long.parseLong((String) sizeObj);
} catch (NumberFormatException ignored) {
log.warn("无法解析文件大小: {}", sizeObj);
}
} else if (sizeObj != null) {
try {
fileSize = Long.parseLong(sizeObj.toString());
} catch (NumberFormatException ignored) {
log.warn("无法解析文件大小对象: {}, 类型: {}", sizeObj, sizeObj.getClass().getName());
}
}
if (mimeTypeObj != null) {
mimeType = String.valueOf(mimeTypeObj);
}
log.info("tempMeta sizeObj={}, sizeObjType={}", sizeObj, sizeObj == null ? "null" : sizeObj.getClass().getName());
}

// 最后再删除临时文件记录,避免把元数据删掉后读不到 size
redisCache.deleteObject(TEMP_FILE_KEY_PREFIX + tempKey);
redisCache.deleteObject(TEMP_FILE_KEY_PREFIX + tempKey + ":meta");

UploadFileMetaVo uploadFileMetaVo = new UploadFileMetaVo(fileName, formalKey, formalUrl, fileSize, mimeType);
log.info("文件转正成功: {} -> {}", tempFileUrl, formalUrl);
return uploadFileMetaVo;

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

//批量 临时 > 正式
@Override
public List<UploadFileMetaVo> 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(uploadService.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 processContentFlies(String content)
{
if (!ImageUrlUtils.containsTempImages(content)) {
return content;
}

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

// 逐个转换为正式URL
String result = content;
for (String tempUrl : tempImageUrls) {
UploadFileMetaVo fileMetaVo = moveTempToFormal(tempUrl);
result = ImageUrlUtils.replaceImageUrl(result, tempUrl, fileMetaVo.getUrl());
}

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

private String extractFileName(String filePath)
{
if (!StringUtils.hasText(filePath)) {
return "";
}

int lastSlashIndex = filePath.lastIndexOf('/');
return lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath;
}

// 正式 > 已删除
@Override
public boolean moveFileToDeleted(String fileUrl)
{
if (!StringUtils.hasText(fileUrl)) {
log.warn("文件URL为空,无法移动到deleted文件夹");
return false;
}

try {
// 从URL中提取文件key
String sourceKey = extractKeyFromUrl(fileUrl);

if (!StringUtils.hasText(sourceKey)) {
log.warn("无法从URL提取文件key: {}", fileUrl);
return false;
}

// 如果文件已经在deleted文件夹中,直接返回成功
if (sourceKey.startsWith(DELETED_PATH_PREFIX)) {
log.info("文件已在deleted文件夹中: {}", sourceKey);
return true;
}

// 生成 deleted key
String targetKey = DELETED_PATH_PREFIX + sourceKey;

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

// 删除源文件
bucketManager.delete(uploadService.getBucket(), sourceKey);

log.info("文件移动到deleted文件夹成功: {} -> {}", sourceKey, targetKey);
return true;

} catch (QiniuException e) {
log.error("文件移动到deleted文件夹失败: {}", fileUrl, e);
return false;
}
}

//批量 正式 > 已删除
@Override
public int batchMoveFilesToDeleted(List<String> fileUrls)
{
if (fileUrls == null || fileUrls.isEmpty()) {
return 0;
}

int successCount = 0;
for (String fileUrl : fileUrls) {
if (moveFileToDeleted(fileUrl)) {
successCount++;
}
}

log.info("批量移动文件到deleted文件夹完成,成功: {}/{}", successCount, fileUrls.size());
return successCount;
}

// 已删除 > 正式 (恢复操作)
@Override
public String restoreFileFromDeleted(String deletedFileUrl)
{
if (!StringUtils.hasText(deletedFileUrl)) {
log.warn("文件URL为空,无法从deleted文件夹恢复");
return null;
}

try {
// 从URL中提取文件key
String deletedKey = extractKeyFromUrl(deletedFileUrl);

if (!StringUtils.hasText(deletedKey)) {
log.warn("无法从URL提取文件key: {}", deletedFileUrl);
return null;
}

// 检查文件是否在deleted文件夹中
if (!deletedKey.startsWith(DELETED_PATH_PREFIX)) {
log.warn("文件不在deleted文件夹中,无需恢复: {}", deletedKey);
return deletedFileUrl; // 返回原URL
}

// 生成恢复后的文件key(去掉deleted/前缀)
String restoredKey = deletedKey.substring(DELETED_PATH_PREFIX.length());

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

// 删除deleted文件夹中的文件
bucketManager.delete(uploadService.getBucket(), deletedKey);

// 返回恢复后的文件URL
String restoredUrl = uploadService.getDomain() + restoredKey;
log.info("文件从deleted文件夹恢复成功: {} -> {}", deletedKey, restoredKey);
return restoredUrl;

} catch (QiniuException e) {
log.error("文件从deleted文件夹恢复失败: {}", deletedFileUrl, e);
return null;
}
}

//批量 已删除 > 正式 (恢复操作)
@Override
public List<String> batchRestoreFilesFromDeleted(List<String> deletedFileUrls)
{
if (deletedFileUrls == null || deletedFileUrls.isEmpty()) {
return new ArrayList<>();
}

List<String> restoredUrls = new ArrayList<>();
int successCount = 0;

for (String deletedFileUrl : deletedFileUrls) {
String restoredUrl = restoreFileFromDeleted(deletedFileUrl);
if (restoredUrl != null) {
restoredUrls.add(restoredUrl);
successCount++;
}
}

log.info("批量从deleted文件夹恢复文件完成,成功: {}/{}", successCount, deletedFileUrls.size());
return restoredUrls;
}
}


存入 文件表的文件名以 UUID + 原始文件名 命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 文件存储工具类
*/
public class PathUtils {
public static String generateFilePath(String fileName){
// 根据日期生成路径,例如:2026/05/07/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
String datePath = sdf.format(new Date());
// uuid作为文件名后缀
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
// 保留原始文件名,并使用下划线拼接uuid,便于和展示名区分
return new StringBuilder()
.append(datePath)
.append(uuid)
.append("_")
.append(fileName)
.toString();
}
}

添加图片类型常量

1
2
3
4
/**
* 文件类型:图片
*/
public static final String FILE_TYPE_IMAGE = "图片";

更新 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
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
//更新用户信息
@Override
public ResponseResult updateUserInfo(UpdateUserInfoDto updateUserInfoDto)
{
Long currentUserId = SecurityUtils.getUserId();
if (currentUserId == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}

//昵称不能为空
if (!StringUtils.hasText(updateUserInfoDto.getNickname())) {
return ResponseResult.errorResult(AppHttpCodeEnum.NICKNAME_NOT_NULL, "昵称不能为空");
}

//XSS防护:转义昵称
String safeNickname = XssUtils.escapeHtml(updateUserInfoDto.getNickname());
updateUserInfoDto.setNickname(safeNickname);

//敏感词检测 - 昵称
if (sensitiveUtilService.checkSensitiveWordsSimple(updateUserInfoDto.getNickname())) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "昵称包含敏感词,请修改后重试");
}

//邮箱和电话号码不能同时为空
if (!StringUtils.hasText(updateUserInfoDto.getEmail()) && !StringUtils.hasText(updateUserInfoDto.getPhone())) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "邮箱和电话号码不能同时为空");
}

//校验手机号格式
if (StringUtils.hasText(updateUserInfoDto.getPhone()) && !updateUserInfoDto.getPhone().matches(PHONE_REGEX)) {
return ResponseResult.errorResult(AppHttpCodeEnum.PHONE_FORMAT_ERROR, "请输入正确的手机号码");
}

//检查手机号是否被其他用户使用
if (StringUtils.hasText(updateUserInfoDto.getPhone())) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getPhone, updateUserInfoDto.getPhone())
.ne(SysUser::getId, currentUserId);
SysUser existSysUser = getOne(queryWrapper);
if (existSysUser != null) {
return ResponseResult.errorResult(AppHttpCodeEnum.PHONE_OCCUPIED, "你用别人的电话号码干嘛?");
}
}

//检查邮箱是否被其他用户使用
if (StringUtils.hasText(updateUserInfoDto.getEmail())) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getEmail, updateUserInfoDto.getEmail())
.ne(SysUser::getId, currentUserId);
SysUser existSysUser = getOne(queryWrapper);
if (existSysUser != null) {
return ResponseResult.errorResult(AppHttpCodeEnum.EMAIL_OCCUPIED, "你用别人的邮箱干嘛?");
}
}

//检查昵称是否被其他用户使用
if (StringUtils.hasText(updateUserInfoDto.getNickname())) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getNickname, updateUserInfoDto.getNickname())
.ne(SysUser::getId, currentUserId);
SysUser existSysUser = getOne(queryWrapper);
if (existSysUser != null) {
return ResponseResult.errorResult(AppHttpCodeEnum.NICKNAME_OCCUPIED, "这个昵称和别人的重复啦!");
}
}

//获取更新前的用户信息,用于判断头像和昵称是否变化
SysUser oldSysUser = getById(currentUserId);

//处理头像:如果是临时文件,转为正式文件
String avatar = updateUserInfoDto.getAvatar();
if (StringUtils.hasText(avatar) && avatar.contains("temp/")) {
UploadFileMetaVo uploadFileMetaVo = ossFileService.moveTempToFormal(avatar);
avatar = uploadFileMetaVo.getUrl();

//根据当前用户和头像来源查找文件记录,存在则更新,不存在则新增
if (StringUtils.hasText(avatar)) {
LambdaQueryWrapper<SysFile> fileQueryWrapper = new LambdaQueryWrapper<>();
String fileSource = "用户 " + oldSysUser.getUsername() + " 的头像图片";
fileQueryWrapper.eq(SysFile::getCreateBy, currentUserId)
.eq(SysFile::getFileSource, fileSource)
.last("LIMIT 1");
SysFile avatarFile = sysFileService.getOne(fileQueryWrapper);

if (avatarFile == null) {
avatarFile = new SysFile();
avatarFile.setName(uploadFileMetaVo.getName());
avatarFile.setFileSource(fileSource);
avatarFile.setType(SystemConstants.FILE_TYPE_IMAGE);
avatarFile.setFilePath(uploadFileMetaVo.getFilePath());
avatarFile.setUrl(uploadFileMetaVo.getUrl());
avatarFile.setSize(uploadFileMetaVo.getSize());
avatarFile.setMimeType(uploadFileMetaVo.getMimeType());
avatarFile.setCreateBy(currentUserId);
sysFileService.save(avatarFile);
} else {
avatarFile.setName(uploadFileMetaVo.getName());
avatarFile.setFileSource(fileSource);
avatarFile.setType(SystemConstants.FILE_TYPE_IMAGE);
avatarFile.setFilePath(uploadFileMetaVo.getFilePath());
avatarFile.setUrl(uploadFileMetaVo.getUrl());
avatarFile.setSize(uploadFileMetaVo.getSize());
avatarFile.setMimeType(uploadFileMetaVo.getMimeType());
avatarFile.setUpdateBy(currentUserId);
sysFileService.updateById(avatarFile);
}
}
}

//如果头像发生变化,删除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, updateUserInfoDto.getNickname())
.set(SysUser::getSex, updateUserInfoDto.getSex())
.set(SysUser::getPhone, StringUtils.hasText(updateUserInfoDto.getPhone()) ? updateUserInfoDto.getPhone() : null)
.set(SysUser::getEmail, StringUtils.hasText(updateUserInfoDto.getEmail()) ? updateUserInfoDto.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 = !updateUserInfoDto.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, updateUserInfoDto.getNickname());
}

commentMapper.update(null, commentWrapper);
}

return ResponseResult.okResult();
}




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

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


预告

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

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

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


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


『博客开发日记』之修改前台上传接口的方法
http://example.com/2026/05/08/『博客开发日记』之修改前台上传接口的方法/
作者
云梦泽
发布于
2026年5月8日
更新于
2026年5月8日
许可协议