完善头像上传模块,基于Minio实现

This commit is contained in:
柏码の讲师 2023-09-20 01:39:48 +08:00
parent 9f15a84f78
commit ca442c7e67
18 changed files with 245 additions and 8 deletions

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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)
)

View File

@ -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, "头像上传失败!");
}
}
}

View File

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

View File

@ -22,5 +22,6 @@ public class Account implements BaseData {
String password;
String email;
String role;
String avatar;
Date registerTime;
}

View File

@ -9,5 +9,6 @@ public class AccountVO {
String username;
String email;
String role;
String avatar;
Date registerTime;
}

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

@ -39,3 +39,7 @@ spring:
origin: '*'
credentials: false
methods: '*'
minio:
endpoint: 'http://localhost:9000'
username: 'minio'
password: 'password'

View File

@ -43,3 +43,7 @@ spring:
origin: '*'
credentials: false
methods: '*'
minio:
endpoint: 'http://localhost:9000'
username: 'minio'
password: 'password'

View File

@ -99,4 +99,4 @@ function unauthorized() {
return !takeAccessToken()
}
export { post, get, login, logout, unauthorized }
export { post, get, login, logout, unauthorized, accessHeader }

View File

@ -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'
}
}
})

View File

@ -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>

View File

@ -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">

View File

@ -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>