完善头像上传模块,基于Minio实现
This commit is contained in:
parent
9f15a84f78
commit
ca442c7e67
@ -94,6 +94,12 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.1.0</version>
|
||||
</dependency>
|
||||
<!-- 对象存储 -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>8.3.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
|
@ -0,0 +1,29 @@
|
||||
package com.example.config;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class MinioConfiguration {
|
||||
|
||||
@Value("${spring.minio.endpoint}")
|
||||
String endpoint;
|
||||
@Value("${spring.minio.username}")
|
||||
String username;
|
||||
@Value("${spring.minio.password}")
|
||||
String password;
|
||||
|
||||
@Bean
|
||||
public MinioClient minioClient() {
|
||||
log.info("Init minio client...");
|
||||
return MinioClient
|
||||
.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentials(username, password)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ public class SecurityConfiguration {
|
||||
return http
|
||||
.authorizeHttpRequests(conf -> conf
|
||||
.requestMatchers("/api/auth/**", "/error").permitAll()
|
||||
.requestMatchers("/images/**").permitAll()
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
|
||||
)
|
||||
|
@ -0,0 +1,35 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.service.ImageService;
|
||||
import com.example.utils.Const;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/image")
|
||||
public class ImageController {
|
||||
|
||||
@Resource
|
||||
ImageService service;
|
||||
|
||||
@PostMapping("/avatar")
|
||||
public RestBean<String> uploadAvatar(@RequestParam("file") MultipartFile file,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int id) throws IOException {
|
||||
if(file.getSize() > 1024 * 100)
|
||||
return RestBean.failure(400, "头像图片不能大于100KB");
|
||||
log.info("正在进行头像上传操作...");
|
||||
String url = service.uploadAvatar(id, file);
|
||||
if(url != null) {
|
||||
log.info("成功上传头像图片,大小: "+file.getSize());
|
||||
return RestBean.success(url);
|
||||
} else {
|
||||
return RestBean.failure(400, "头像上传失败!");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.service.ImageService;
|
||||
import io.minio.errors.ErrorResponseException;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class ObjectController {
|
||||
|
||||
@Resource
|
||||
ImageService service;
|
||||
|
||||
@GetMapping("/images/avatar/**")
|
||||
public void imagesFetch(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
response.setHeader("Cache-Control", "max-age=2592000");
|
||||
this.fetchImage(request, response);
|
||||
}
|
||||
|
||||
private void fetchImage(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String imagePath = request.getServletPath().substring(7);
|
||||
ServletOutputStream stream = response.getOutputStream();
|
||||
if(imagePath.isBlank() || imagePath.isEmpty()) {
|
||||
stream.println(RestBean.failure(404, "image name must be a non-empty string").toString());
|
||||
} else {
|
||||
try {
|
||||
service.fetchImageFromMinio(stream, imagePath);
|
||||
} catch (ErrorResponseException e) {
|
||||
if(e.response().code() == 404) {
|
||||
response.setStatus(404);
|
||||
stream.println(RestBean.failure(404, "Not found").toString());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,5 +22,6 @@ public class Account implements BaseData {
|
||||
String password;
|
||||
String email;
|
||||
String role;
|
||||
String avatar;
|
||||
Date registerTime;
|
||||
}
|
||||
|
@ -9,5 +9,6 @@ public class AccountVO {
|
||||
String username;
|
||||
String email;
|
||||
String role;
|
||||
String avatar;
|
||||
Date registerTime;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class RequestLogFilter extends OncePerRequestFilter {
|
||||
@Resource
|
||||
SnowflakeIdGenerator generator;
|
||||
|
||||
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs");
|
||||
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs", "/images");
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
|
@ -0,0 +1,11 @@
|
||||
package com.example.service;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface ImageService {
|
||||
String uploadAvatar(int id, MultipartFile file) throws IOException;
|
||||
void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception;
|
||||
}
|
@ -99,7 +99,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
||||
String password = passwordEncoder.encode(info.getPassword());
|
||||
Account account = new Account(null, info.getUsername(),
|
||||
password, email, Const.ROLE_DEFAULT, new Date());
|
||||
password, email, Const.ROLE_DEFAULT, null, new Date());
|
||||
if(!this.save(account)) {
|
||||
return "内部错误,注册失败";
|
||||
} else {
|
||||
|
@ -0,0 +1,65 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.mapper.AccountMapper;
|
||||
import com.example.service.ImageService;
|
||||
import io.minio.GetObjectArgs;
|
||||
import io.minio.GetObjectResponse;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.PutObjectArgs;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ImageServiceImpl implements ImageService {
|
||||
|
||||
@Resource
|
||||
MinioClient client;
|
||||
|
||||
@Resource
|
||||
AccountMapper mapper;
|
||||
|
||||
@Override
|
||||
public void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception {
|
||||
GetObjectArgs args = GetObjectArgs.builder()
|
||||
.bucket("test")
|
||||
.object(imagePath)
|
||||
.build();
|
||||
GetObjectResponse object = client.getObject(args);
|
||||
IOUtils.copy(object, stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadAvatar(int id, MultipartFile file) throws IOException {
|
||||
//使用随机UUID作为图片名字,而不是直接用户ID是因为用户更改头像之后可以立即更新缓存
|
||||
String imageName = UUID.randomUUID().toString().replace("-", "");
|
||||
imageName = "/avatar/" + imageName;
|
||||
PutObjectArgs args = PutObjectArgs.builder()
|
||||
.bucket("test")
|
||||
.stream(file.getInputStream(), file.getSize(), -1)
|
||||
.object(imageName)
|
||||
.build();
|
||||
try {
|
||||
client.putObject(args);
|
||||
if(mapper.update(null, Wrappers.<Account>update()
|
||||
.eq("id", id)
|
||||
.set("avatar", imageName)) > 0) {
|
||||
return imageName;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("图片上传出现问题: "+e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -39,3 +39,7 @@ spring:
|
||||
origin: '*'
|
||||
credentials: false
|
||||
methods: '*'
|
||||
minio:
|
||||
endpoint: 'http://localhost:9000'
|
||||
username: 'minio'
|
||||
password: 'password'
|
||||
|
@ -43,3 +43,7 @@ spring:
|
||||
origin: '*'
|
||||
credentials: false
|
||||
methods: '*'
|
||||
minio:
|
||||
endpoint: 'http://localhost:9000'
|
||||
username: 'minio'
|
||||
password: 'password'
|
||||
|
@ -99,4 +99,4 @@ function unauthorized() {
|
||||
return !takeAccessToken()
|
||||
}
|
||||
|
||||
export { post, get, login, logout, unauthorized }
|
||||
export { post, get, login, logout, unauthorized, accessHeader }
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from "axios";
|
||||
|
||||
export const useStore = defineStore('general', {
|
||||
state: () => {
|
||||
@ -7,8 +8,16 @@ export const useStore = defineStore('general', {
|
||||
username: '',
|
||||
email: '',
|
||||
role: '',
|
||||
registerTime: null
|
||||
registerTime: null,
|
||||
avatar: null
|
||||
}
|
||||
}
|
||||
}, getters: {
|
||||
avatarUrl() {
|
||||
if(this.user.avatar)
|
||||
return `${axios.defaults.baseURL}/images${this.user.avatar}`
|
||||
else
|
||||
return 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -59,7 +59,7 @@ function userLogout() {
|
||||
<div>{{store.user.email}}</div>
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-avatar :size="38" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"/>
|
||||
<el-avatar :size="38" :src="store.avatarUrl"/>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
|
@ -11,7 +11,7 @@ const today = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex;margin: 20px auto;gap: 20px;max-width: 900px">
|
||||
<div style="display: flex;margin: 20px auto;gap: 20px;max-width: 1000px">
|
||||
<div style="flex: 1">
|
||||
<light-card>
|
||||
<div class="edit-topic">
|
||||
|
@ -4,8 +4,9 @@ import Card from "@/components/Card.vue";
|
||||
import {Message, Refresh, Select, User} from "@element-plus/icons-vue";
|
||||
import {useStore} from "@/store";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import {get, post} from "@/net";
|
||||
import {accessHeader, get, post} from "@/net";
|
||||
import {ElMessage} from "element-plus";
|
||||
import axios from "axios";
|
||||
|
||||
const store = useStore();
|
||||
const registerTime = computed(() => new Date(store.user.registerTime).toLocaleString())
|
||||
@ -115,6 +116,22 @@ function modifyEmail() {
|
||||
emailForm.code = ''
|
||||
})
|
||||
}
|
||||
|
||||
function beforeAvatarUpload(rawFile) {
|
||||
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
|
||||
ElMessage.error('头像只能是 JPG/PNG 格式的!')
|
||||
return false
|
||||
} else if (rawFile.size / 1024 > 100) {
|
||||
ElMessage.error('头像大小不能大于 100KB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function uploadSuccess(response) {
|
||||
ElMessage.success('头像上传成功!')
|
||||
store.user.avatar = response.data
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -182,7 +199,18 @@ function modifyEmail() {
|
||||
<card v-loading="loading.form">
|
||||
<div style="text-align: center;padding: 5px 15px 0 15px">
|
||||
<div style="display: inline-block">
|
||||
<el-avatar :size="70" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"/>
|
||||
<el-avatar :size="70" :src="store.avatarUrl"/>
|
||||
<div style="margin: 5px 0">
|
||||
<el-upload
|
||||
:action="axios.defaults.baseURL + '/api/image/avatar'"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:headers="accessHeader()">
|
||||
<el-button size="small" round>修改头像</el-button>
|
||||
</el-upload>
|
||||
|
||||
</div>
|
||||
<div style="font-weight: bold">你好, {{store.user.username}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user