我在 Spring Boot 中实现了一个后端方法,允许用户创建带有或不带有图像的帖子。后端方法如下:
@PostMapping(value = "/posts/add", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@ApiResponse(responseCode = "400", description = "Please check the payload or token")
@ApiResponse(responseCode = "200", description = "Post added with image")
@Operation(summary = "Add a new Post with optional image")
@SecurityRequirement(name = "studyeasy-demo-api")
public ResponseEntity<?> addPostWithImage(
@Parameter(content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) @RequestPart("caption") PostPayloadDTO postPayloadDTO,
@RequestPart(value = "file", required = false) MultipartFile file,
@RequestParam("postTypeName") String postTypeName,
Authentication authentication) {
try {
// Xác thực email và người dùng
String email = authentication.getName();
Optional<Account> optionalAccount = accountService.findByEmail(email);
Users users = optionalAccount.get().getUser();
Post post = new Post();
post.setUser(users);
post.setCaption(postPayloadDTO.getCaption());
PostType postType = postTypeService.getPostTypeByName(postTypeName);
post.setPostType(postType);
postService.save(post);
if (file != null && !file.isEmpty()) {
String contentType = file.getContentType();
if (contentType.equals("image/png") || contentType.equals("image/jpg") || contentType.equals("image/jpeg")) {
String fileName = file.getOriginalFilename();
String generatedString = RandomStringUtils.random(10, true, true);
String finalImageName = generatedString + fileName;
String absoluteFileLocation = AppUtil.get_photo_upload_path(finalImageName, PHOTOS_FOLDER_NAME, post.getPostId());
Path path = Paths.get(absoluteFileLocation);
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
post.setPostImgURL(absoluteFileLocation); // Lưu đường dẫn ảnh vào post
// Tạo đối tượng Photo và lưu thông tin file vào cơ sở dữ liệu
Photo photo = new Photo();
photo.setName(fileName); // Lưu tên gốc của file
photo.setFileName(finalImageName); // Lưu tên file sau khi thêm chuỗi ngẫu nhiên
photo.setOriginalFileName(fileName); // Lưu tên file gốc
photo.setPost(post); // Liên kết ảnh với album
photoService.save(photo); // Lưu ảnh vào database
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Unsupported file format. Please use PNG, JPG, or JPEG.");
}
}
List<PhotoDTO> photos = new ArrayList<>();
for (Photo photo : photoService.findByPostId(post.getPostId())) {
String link = "/posts/" + post.getPostId() + "/photos/" + photo.getId() + "/download-photo";
photos.add(new PhotoDTO(photo.getId(), photo.getName(), photo.getDescription(), photo.getFileName(), link));
}
// Lưu post vào database
post = postService.save(post);
PostViewDTO postViewDTO = new PostViewDTO(post.getPostId(), post.getUser().getProfileImage(), post.getCaption(), post.getUser().getUserName(), photos);
return ResponseEntity.ok(postViewDTO);
} catch (Exception e) {
log.debug("Error adding post with image: " + e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
}
我已经使用 Postman 测试了后端 API,无论有图像还是没有图像,它都能按预期工作。
但是,当我尝试从前端(React)调用相同的 API 时,我在浏览器控制台中收到以下错误:
POST http://localhost:3000/api/v1/posts/add 415 (Unsupported Media Type)
在后端日志中,我看到:
[org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported]
这是我的 React 前端的相关代码:
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { fetchPostFileUploadWithAuth } from 'client/client';
const Share = () => {
const [file, setFile] = useState(null);
const [description, setDescription] = useState('');
const navigate = useNavigate();
const handleShare = async () => {
const formData = new FormData();
formData.append('caption', description);
formData.append('postTypeName', 'normal');
if (file) {
formData.append('file', file);
}
try {
await fetchPostFileUploadWithAuth('/posts/add', formData);
setDescription('');
setFile(null);
navigate('/');
} catch (error) {
console.error('Failed to create post:', error);
}
};
return (
<div className="share">
{/* JSX for file input, caption, etc. */}
</div>
);
};
export default Share;
当我尝试上传任何内容(带或不带图像)时,就会出现此问题。后端方法正确设置为消耗
multipart/form-data
,我在前端使用 FormData
来发送请求。
我使用以下函数进行 API 调用:
const fetchPostFileUploadWithAuth = async (uri, formData) => {
const token = localStorage.getItem('token');
const url = `${API_version}${uri}`;
try {
const response = await axios.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
"Authorization": `Bearer ${token}`,
},
});
return response;
} catch (error) {
// Handle errors if the request fails
console.error('Error fetching data:', error);
}
};
嗯,这个错误很熟悉,因为它让我彻夜难眠。 415 不支持的媒体类型。我认为这意味着请求的内容类型与 spring-boot 期望的不匹配。既然您已经确认您的 BE API 可以工作,我怀疑问题在于 React 如何发出请求。只是在这里做出有根据的猜测。您可以尝试以下一些方法:
检查
FormData
中的字段名称是否与控制器中的参数名称匹配。 FormData
(caption
、file
和postTypeName
)
请检查您的
axios
版本。我把它放在这里是因为 axios
的某些版本存在 FormData
的已知问题
删除
Content-Type
标题,我认为是因为 axios
自动将 Content-Type
标题设置为带有边界的 multipart/form-data
,这对于 FormData
正常工作至关重要。手动设置 Content-Type
可能会干扰此操作。因此,尝试从标题中删除 'Content-Type': 'multipart/form-data'
,如下所示:
const response = await axios.post(url, formData, {
headers: {
"Authorization": `Bearer ${token}`,
},
});
postTypeName
中设置 FormData
而不是 RequestParam
:由于您当前已将 postTypeName
添加为 RequestParam
,因此它可能无法在 FormData
对象中按预期工作。相反,请确保将 postTypeName
添加为控制器中的 RequestPart
参数。你明白吗?我也会像这样改进你的
addPostWithImage
端点。
@PostMapping(value = "/posts/add", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@ApiResponse(responseCode = "400", description = "Please check the payload or token")
@ApiResponse(responseCode = "200", description = "Post added with image")
@Operation(summary = "Add a new Post with optional image")
@SecurityRequirement(name = "studyeasy-demo-api")
public ResponseEntity<?> addPostWithImage(
@Parameter(content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) @RequestPart("caption") PostPayloadDTO postPayloadDTO,
@RequestPart(value = "file", required = false) MultipartFile file,
@RequestPart("postTypeName") String postTypeName, // I changed this from RequestParam to RequestPart
Authentication authentication) {
...
}
这些是我可以立即通过您的代码发现的一些问题。尝试更改它们,请告诉我。