1 概述
新做的博客系统需要在 markdown
文本中插入图片, 之前完成过上传图片的相关配置,但未做总结,借着这个机会,对于 springboot
上传图片接口的相关配置和操作,做一个系统性阐述。以作为未来相关业务的参考。
本文主要阐述后端相关配置,少量前端(vue3)内容仅是为了作为测试。
2 配置文件
配置相关信息仅需两步:
- 在
yaml
文件中配置 相关路径 和 静态资源。 - 在配置类中配置静态资源处理器。
2.1 yaml 文件配置
为保证上传路径的 可配置性,这里的上传路径相关字符串全部配置在 application.yaml
文件中,然后再使用 @Value()
注解注入即可。
注:无关配置已省略
upload:
upload-path: ./upload
image:
user: ${upload.upload-path}/image/avatar # 用户图片(包含头像和封面)路径
common: ${upload.upload-path}/image/common # 通用图片(主要是文章公告等图片)路径
spring:
# 配置静态资源路径
web:
resources:
static-locations: classpath:/static/, file:${upload.upload-path}
# 限制上传文件最大值
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
2.2 配置类
这里主要配置静态资源处理器,可以理解为请求 url
到文件路径的映射。
/**
* @author gs_huang
* @date 2024/4/9 11:19
*/
@Configuration
public class ResourceConfig implements WebMvcConfigurer {
@Value("${upload.upload-path}/")
private String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + uploadPath);
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
3 保存图片
配置好上传路径和静态资源处理器后,就需要向外提供保存图片的接口了。
这里需要在 service
层和 controller
层提供相应的保存图片以及把相关数据保存至数据库的方法。
3.1 service 层
1、service
接口:
public interface PictureService extends IService<Picture> {
/**
* @param url 请求 url
* @param file 图片文件
* @return 上传反馈
*/
Picture uploadImage(String url, MultipartFile file);
}
注意我这里继承 IService<Picture>
是因为我使用了 Mybatis-Plus,不影响本文阐述功能。
2、serviceImpl
实现类:
@Service
public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
implements PictureService{
private final Logger logger = LoggerFactory.getLogger(PictureServiceImpl.class);
@Value("${upload.image.common}/")
private String commonPath;
/**
* 日期路径格式
*/
private final String datePathFormat = "yyyy/MM/dd/";
/**
* 日期格式
*/
private final SimpleDateFormat sdf = new SimpleDateFormat(datePathFormat);
@Override
public Picture uploadImage(String url, MultipartFile file) {
//1.获取日期字符串
String formatDate = sdf.format(new Date());
//2.获取新文件名
String newFileName = getNewFileName(file.getOriginalFilename());
//3.保存图片
//3.1 判断文件夹是否存在,不存在则创建
String imageDirPath = commonPath + formatDate;
File imageDir = new File(imageDirPath);
if (!imageDir.exists()){
imageDir.mkdirs();
}
//3.2 拼接文件完整路径
String imageFilePath = imageDirPath + "/" + newFileName;
//3.3 保存
try {
file.transferTo(new File(imageDir.getAbsoluteFile(), newFileName));
} catch (Exception e){
logger.error("文件 {} 保存失败", imageFilePath, e);
return null;
}
//4.拼接请求路径
//4.1 正则匹配请求前缀
Pattern pattern = Pattern.compile("(.*)/admin/picture.*");
Matcher matcher = pattern.matcher(url);
String urlPrefix = "http://localhost:18080";
while(matcher.find()){
urlPrefix = matcher.group(1);
}
String urlPath = urlPrefix + "/upload/image/common/" + formatDate + newFileName;
//5.构造存储数据
Picture picture = new Picture();
Date nowDate = new Date();
picture.setCreated(nowDate);
picture.setEdited(nowDate);
picture.setPath(urlPath);
picture.setStatus(1);
String pictureId = IdWorker.getIdStr(picture);
picture.setId(pictureId);
//6.图片数据存入数据库
save(picture);
//7.返回图片信息
return picture;
}
/**
* 根据旧文件名生成新文件名,使用 uuid 生成
* @param oldName 旧文件名
* @return 新文件名
*/
public String getNewFileName(String oldName){
//1.旧名称判空(这里使用了 hutools)
if (StrUtil.isEmpty(oldName)){
return null;
}
//2.正则匹配获取文件后缀
Pattern sufixPattern = Pattern.compile(".*(\\..*)");
Matcher matcher = sufixPattern.matcher(oldName);
//3.UUID 生成文件新名称(这里使用了 hutools)
String newFileName = UUID.randomUUID().toString();
if (matcher.find()){
newFileName += matcher.group(1);
}
//4.返回新名称
return newFileName;
}
}
3.2 controller 层
/**
* @author gs_huang
* @date 2024/4/9 9:52
*/
@RestController
@RequestMapping("/admin/picture")
public class PictureController {
@Autowired
private PictureService pictureService;
@PostMapping("/uploadImage")
public Result uploadImage(HttpServletRequest request, @RequestParam("file") MultipartFile file){
if (file == null) {
return Result.formatError("文件错误");
}
Picture picture = pictureService.uploadImage(request.getRequestURL().toString(), file);
return picture != null ? Result.success(picture) : Result.error("上传图片失败");
}
}
4 前端调用
这里我开发的是 vue3
整合 v-md-editor 后,markdown 文本上传图片的功能,所以测试也是使用的其提供的回调方法。
这里有个坑,即 v-md-editor
提供的上传图片回调方法必须使用 formdata
格式上传,否则后端会报错!
4.1 api 接口
以下我自己封装的 axios
实例 http.js
,如果你有自己的实例,可以忽略以下代码.
// axios 基础封装
import axios from 'axios'
import { authStore } from '@/stores/auth'
import router from '@/router'
const httpInstance = axios.create({
baseURL: 'http://localhost:18081',
timeout: 5000
})
// 拦截器
// axios 请求拦截器
httpInstance.interceptors.request.use(config => {
config.headers['token'] = authStore().token
return config
}, e => Promise.reject(e))
// axios 响应拦截器
httpInstance.interceptors.response.use(res => {
if ("token" in res.headers) {
authStore().setToken(res.headers["token"])
}
if (res.data.code === 401) {
router.push('/login')
authStore().removeToken();
authStore().removeUserAuth();
}
return res.data
}, e => {
return Promise.reject(e)
})
// 暴露出请求实例
export default httpInstance
这里会用到上述 httpInstance 实例。
picture.js
:
import httpInstance from '@/utils/http'
export function uploadImageAPI(file) {
return httpInstance({
url: `/admin/picture/uploadImage`,
method: 'post',
data: file
})
}
4.2 调用接口
这里会忽略掉无关代码。
AddNotice.vue
:
<script setup>
import { ElMessage } from "element-plus";
import { uploadImageAPI } from "@/apis/admin/picture";
const uploadImage = async (insertImage, file) => {
const formData = new FormData();
formData.append("file", file);
const res = await uploadImageAPI(formData);
if (res.code === 200) {
insertImage({
url: res.data.path,
desc: res.data.id,
});
ElMessage({
type: "success",
message: "图片上传成功",
plain: true,
});
} else {
ElMessage({
type: "error",
message: res.msg,
plain: true,
});
}
};
// 上传图片
const handleUploadImage = (event, insertImage, files) => {
console.log(files[0]);
uploadImage(insertImage, files[0]);
};
</script>
<template>
<div class="add-notice">
<v-md-editor
v-model="addNotice.content"
height="calc(100% - 150px)"
:include-level="[1, 2, 3]"
@save="saveLocal"
@blur="saveLocal"
:disabled-menus="[]"
@upload-image="handleUploadImage"
></v-md-editor>
</div>
</template>
5 效果展示
6 踩坑记录
对于 v-md-editor
中图片上传的数据格式,必须要使用 formdata
来进行封装才行,而不是一味的修改请求 headers
中的 Content-Type
。
封装 formdata 关键代码:
const formData = new FormData();
formData.append("file", file);
封装完成后即可将 formdata 作为 file 对象传输给后端。
但是对于 elementUI 中的文件上传,则可以使用原格式,不需要封装 formdata,即修改 headers 即可。
修改 headers 示例如下:
export function uploadImage(file) {
return request({
headers: {
'Content-Type': 'multipart/form-data',
},
url: `/admin/picture/uploadImage`,
method: 'post',
data: file
})
}
7 写在最后
v-md-editor 官网:https://code-farmer-i.github.io/vue-markdown-editor/zh/
关于 v-md-editor 图片上传,参考:https://code-farmer-i.github.io/vue-markdown-editor/zh/senior/upload-image.html
最后感慨一句,vue3 整合 v-md-editor 成功后,是真的帅啊!
标签:picture,return,SpringBoot,upload,String,file,上传,图片 From: https://www.cnblogs.com/huang-guosheng/p/18146420