feat(admin): 新增博客设置及文件上传功能

- 新增博客设置数据库表结构及实体类定义
- 实现博客设置的查询与更新接口及服务层逻辑
- 实现管理端博客设置控制器,支持修改和查询博客设置详情
- 实现文件上传接口和服务,支持通过Rustfs上传文件
- 集成Rustfs客户端配置,支持与Rustfs存储系统交互
- 新增统一响应及异常处理,文件上传异常抛出自定义业务异常
- 更新错误码枚举,添加文件上传失败的错误码定义
- 增加请求参数校验,确保博客设置更新接口数据有效性
- 添加日志记录,跟踪文件上传流程及错误信息
This commit is contained in:
2025-12-05 22:14:47 +08:00
parent 304c458436
commit 7db42c6c30
17 changed files with 612 additions and 1 deletions

View File

@@ -0,0 +1,43 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
import com.hanserwei.admin.service.AdminBlogSettingsService;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 管理端博客设置控制器
*/
@RestController
@RequestMapping("/admin/blog/settings")
public class AdminBlogSettingsController {
@Resource
private AdminBlogSettingsService blogSettingsService;
/**
* 博客基础信息修改
*/
@PostMapping("/update")
@ApiOperationLog(description = "博客基础信息修改")
public Response<?> updateBlogSettings(@RequestBody @Validated UpdateBlogSettingsReqVO updateBlogSettingsReqVO) {
return blogSettingsService.updateBlogSettings(updateBlogSettingsReqVO);
}
/**
* 获取博客设置详情
*/
@PostMapping("/detail")
@ApiOperationLog(description = "获取博客设置详情")
public Response<FindBlogSettingsRspVO> findDetail() {
return blogSettingsService.findDetail();
}
}

View File

@@ -0,0 +1,33 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
import com.hanserwei.admin.service.AdminFileService;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 管理端文件控制器
*/
@RestController
@RequestMapping("/admin")
public class AdminFileController {
@Resource
private AdminFileService fileService;
/**
* 文件上传
*/
@PostMapping("/file/upload")
@ApiOperationLog(description = "文件上传")
public Response<UploadFileRspVO> uploadFile(@RequestParam("file") MultipartFile file) {
return fileService.uploadFile(file);
}
}

View File

@@ -0,0 +1,19 @@
package com.hanserwei.admin.model.vo.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UploadFileRspVO {
/**
* 文件的访问链接
*/
private String url;
}

View File

@@ -0,0 +1,62 @@
package com.hanserwei.admin.model.vo.setting;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 查询博客设置响应 VO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindBlogSettingsRspVO {
/**
* 博客 Logo
*/
private String logo;
/**
* 博客名称
*/
private String name;
/**
* 作者名称
*/
private String author;
/**
* 博客介绍
*/
private String introduction;
/**
* 作者头像
*/
private String avatar;
/**
* GitHub 主页地址
*/
private String githubHomepage;
/**
* CSDN 主页地址
*/
private String csdnHomepage;
/**
* Gitee 主页地址
*/
private String giteeHomepage;
/**
* 知乎主页地址
*/
private String zhihuHomepage;
}

View File

@@ -0,0 +1,68 @@
package com.hanserwei.admin.model.vo.setting;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 更新博客设置请求 VO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdateBlogSettingsReqVO {
/**
* 博客 LOGO
*/
@NotBlank(message = "博客 LOGO 不能为空")
private String logo;
/**
* 博客名称
*/
@NotBlank(message = "博客名称不能为空")
private String name;
/**
* 博客作者
*/
@NotBlank(message = "博客作者不能为空")
private String author;
/**
* 博客介绍语
*/
@NotBlank(message = "博客介绍语不能为空")
private String introduction;
/**
* 博客头像
*/
@NotBlank(message = "博客头像不能为空")
private String avatar;
/**
* GitHub 主页
*/
private String githubHomepage;
/**
* CSDN 主页
*/
private String csdnHomepage;
/**
* Gitee 主页
*/
private String giteeHomepage;
/**
* 知乎主页
*/
private String zhihuHomepage;
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
import com.hanserwei.common.utils.Response;
public interface AdminBlogSettingsService {
/**
* 更新博客设置信息
*
* @param updateBlogSettingsReqVO 博客设置信息
* @return 响应
*/
Response<?> updateBlogSettings(UpdateBlogSettingsReqVO updateBlogSettingsReqVO);
/**
* 获取博客设置详情
*
* @return 博客设置详情
*/
Response<FindBlogSettingsRspVO> findDetail();
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
import com.hanserwei.common.utils.Response;
import org.springframework.web.multipart.MultipartFile;
public interface AdminFileService {
/**
* 上传文件
*
* @param file 文件
* @return 访问地址
*/
Response<UploadFileRspVO> uploadFile(MultipartFile file);
}

View File

@@ -0,0 +1,50 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
import com.hanserwei.admin.service.AdminBlogSettingsService;
import com.hanserwei.common.domain.dataobject.BlogSettings;
import com.hanserwei.common.domain.repository.BlogSettingsRepository;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@Service
public class AdminBlogSettingsServiceImpl implements AdminBlogSettingsService {
@Resource
private BlogSettingsRepository blogSettingsRepository;
@Override
public Response<?> updateBlogSettings(UpdateBlogSettingsReqVO updateBlogSettingsReqVO) {
// 保存或更新博客设置
blogSettingsRepository.findById(1L)
.ifPresentOrElse(existingSettings -> {
// 如果存在,则更新现有记录
BeanUtils.copyProperties(updateBlogSettingsReqVO, existingSettings);
blogSettingsRepository.saveAndFlush(existingSettings);
}, () -> {
// 如果不存在,则创建新记录
BlogSettings blogSettings = new BlogSettings();
BeanUtils.copyProperties(updateBlogSettingsReqVO, blogSettings);
blogSettings.setId(1L);
blogSettingsRepository.saveAndFlush(blogSettings);
});
return Response.success();
}
@Override
public Response<FindBlogSettingsRspVO> findDetail() {
return blogSettingsRepository.findById(1L)
.map(e -> {
FindBlogSettingsRspVO findBlogSettingsRspVO = new FindBlogSettingsRspVO();
BeanUtils.copyProperties(e, findBlogSettingsRspVO);
return Response.success(findBlogSettingsRspVO);
})
.orElse(Response.success(null));
}
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
import com.hanserwei.admin.service.AdminFileService;
import com.hanserwei.admin.utils.RustfsUtils;
import com.hanserwei.common.enums.ResponseCodeEnum;
import com.hanserwei.common.exception.BizException;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
public class AdminFileServiceImpl implements AdminFileService {
@Resource
private RustfsUtils rustfsUtils;
@Override
public Response<UploadFileRspVO> uploadFile(MultipartFile file) {
try {
// 上传文件
String url = rustfsUtils.uploadFile(file);
return Response.success(UploadFileRspVO.builder().url(url).build());
} catch (Exception e) {
log.error("==> 上传文件异常:{} ...", e.getMessage());
throw new BizException(ResponseCodeEnum.FILE_UPLOAD_FAILED);
}
}
}

View File

@@ -0,0 +1,72 @@
package com.hanserwei.admin.utils;
import com.hanserwei.common.config.RustfsProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.util.UUID;
@Component
@Slf4j
public class RustfsUtils {
@Resource
private RustfsProperties rustfsProperties;
@Resource
private S3Client s3Client;
/**
* 上传文件
*
* @param file 文件
* @return 访问地址
* @throws Exception 异常
*/
public String uploadFile(MultipartFile file) throws Exception {
// 判断文件是否为空
if (file == null || file.getSize() == 0) {
log.error("==> 上传文件异常:文件大小为空 ...");
throw new RuntimeException("文件大小不能为空");
}
// 文件的原始名称
String originalFileName = file.getOriginalFilename();
// 文件的 Content-Type
String contentType = file.getContentType();
// 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
String key = UUID.randomUUID().toString().replace("-", "");
// 获取文件的后缀,如 .jpg
String suffix = null;
if (originalFileName != null) {
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
}
// 拼接上文件后缀,即为要存储的文件名
String objectName = String.format("%s%s", key, suffix);
log.info("==> 开始上传文件至 Rustfs, ObjectName: {}", objectName);
// 上传文件至 Rustfs
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.key(objectName)
.bucket(rustfsProperties.getBucketName())
.contentType(contentType)
.contentLength(file.getSize())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
// 返回文件的访问链接
String url = String.format("%s/%s/%s", rustfsProperties.getEndpoint(), rustfsProperties.getBucketName(), objectName);
log.info("==> 上传文件至 Rustfs 成功,访问路径: {}", url);
return url;
}
}