完成头像上传
This commit is contained in:
parent
aba88b26a2
commit
915718a07d
@ -94,6 +94,12 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>2.1.0</version>
|
<version>2.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 对象存储Minio依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>8.3.9</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
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
|
return http
|
||||||
.authorizeHttpRequests(conf -> conf
|
.authorizeHttpRequests(conf -> conf
|
||||||
.requestMatchers("/api/auth/**", "/error").permitAll()
|
.requestMatchers("/api/auth/**", "/error").permitAll()
|
||||||
|
.requestMatchers("/images/**").permitAll()
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
|
.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() > 1025 * 100)
|
||||||
|
return RestBean.failure(400, "头像图片不能大于100KB");
|
||||||
|
log.info("正在进行头像上传操作...");
|
||||||
|
String url = service.uploadAvatar(file, id);
|
||||||
|
if(url != null) {
|
||||||
|
log.info("头像上传成功,大小: " + file.getSize());
|
||||||
|
return RestBean.success(url);
|
||||||
|
} else {
|
||||||
|
return RestBean.failure(400, "头像上传失败,请联系管理员!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
public class ObjectController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
ImageService service;
|
||||||
|
|
||||||
|
@GetMapping("/images/avatar/**")
|
||||||
|
public void imageFetch(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||||
|
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.length() <= 13) {
|
||||||
|
response.setStatus(404);
|
||||||
|
stream.println(RestBean.failure(404, "Not found").toString());
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
service.fetchImageFromMinio(stream, imagePath);
|
||||||
|
response.setHeader("Cache-Control", "max-age=2592000");
|
||||||
|
} catch (ErrorResponseException e) {
|
||||||
|
if(e.response().code() == 404) {
|
||||||
|
response.setStatus(404);
|
||||||
|
stream.println(RestBean.failure(404, "Not found").toString());
|
||||||
|
} else {
|
||||||
|
log.error("从Minio获取图片出现异常: "+e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,5 +22,6 @@ public class Account implements BaseData {
|
|||||||
String password;
|
String password;
|
||||||
String email;
|
String email;
|
||||||
String role;
|
String role;
|
||||||
|
String avatar;
|
||||||
Date registerTime;
|
Date registerTime;
|
||||||
}
|
}
|
||||||
|
@ -9,5 +9,6 @@ public class AccountVO {
|
|||||||
String username;
|
String username;
|
||||||
String email;
|
String email;
|
||||||
String role;
|
String role;
|
||||||
|
String avatar;
|
||||||
Date registerTime;
|
Date registerTime;
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ public class RequestLogFilter extends OncePerRequestFilter {
|
|||||||
@Resource
|
@Resource
|
||||||
SnowflakeIdGenerator generator;
|
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
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
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(MultipartFile file, int id) throws IOException;
|
||||||
|
void fetchImageFromMinio(OutputStream stream, String image) throws Exception;
|
||||||
|
}
|
@ -99,7 +99,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
||||||
String password = passwordEncoder.encode(info.getPassword());
|
String password = passwordEncoder.encode(info.getPassword());
|
||||||
Account account = new Account(null, info.getUsername(),
|
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)) {
|
if(!this.save(account)) {
|
||||||
return "内部错误,注册失败";
|
return "内部错误,注册失败";
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
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 image) throws Exception {
|
||||||
|
GetObjectArgs args = GetObjectArgs.builder()
|
||||||
|
.bucket("study")
|
||||||
|
.object(image)
|
||||||
|
.build();
|
||||||
|
GetObjectResponse response = client.getObject(args);
|
||||||
|
IOUtils.copy(response, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String uploadAvatar(MultipartFile file, int id) throws IOException {
|
||||||
|
String imageName = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
imageName = "/avatar/" + imageName;
|
||||||
|
PutObjectArgs args = PutObjectArgs.builder()
|
||||||
|
.bucket("study")
|
||||||
|
.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: '*'
|
origin: '*'
|
||||||
credentials: false
|
credentials: false
|
||||||
methods: '*'
|
methods: '*'
|
||||||
|
minio:
|
||||||
|
endpoint: 'http://localhost:9000'
|
||||||
|
username: 'minio'
|
||||||
|
password: 'password'
|
||||||
|
@ -43,3 +43,7 @@ spring:
|
|||||||
origin: '*'
|
origin: '*'
|
||||||
credentials: false
|
credentials: false
|
||||||
methods: '*'
|
methods: '*'
|
||||||
|
minio:
|
||||||
|
endpoint: 'http://localhost:9000'
|
||||||
|
username: 'minio'
|
||||||
|
password: 'password'
|
||||||
|
@ -99,4 +99,4 @@ function unauthorized() {
|
|||||||
return !takeAccessToken()
|
return !takeAccessToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
export { post, get, login, logout, unauthorized }
|
export { post, get, login, logout, unauthorized, accessHeader }
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export const useStore = defineStore('general', {
|
export const useStore = defineStore('general', {
|
||||||
state: () => {
|
state: () => {
|
||||||
@ -7,8 +8,16 @@ export const useStore = defineStore('general', {
|
|||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: '',
|
role: '',
|
||||||
|
avatar: null,
|
||||||
registerTime: null
|
registerTime: 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>{{store.user.email}}</div>
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown>
|
<el-dropdown>
|
||||||
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"/>
|
<el-avatar :src="store.avatarUrl"/>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-item>
|
<el-dropdown-item>
|
||||||
<el-icon><Operation/></el-icon>
|
<el-icon><Operation/></el-icon>
|
||||||
|
@ -4,8 +4,9 @@ import Card from "@/components/Card.vue";
|
|||||||
import {Message, Refresh, Select, User} from "@element-plus/icons-vue";
|
import {Message, Refresh, Select, User} from "@element-plus/icons-vue";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {computed, reactive, ref} from "vue";
|
import {computed, reactive, ref} from "vue";
|
||||||
import {get, post} from "@/net";
|
import {accessHeader, get, post} from "@/net";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ function saveDetails() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get('/api/user/details', data => {
|
get('/api/user/details', data => {
|
||||||
baseForm.username = store.user.username
|
baseForm.username = store.user.username
|
||||||
baseForm.gender = data.gender
|
baseForm.gender = data.gender
|
||||||
@ -83,6 +85,7 @@ const onValidate = (prop, isValid) => {
|
|||||||
if (prop === 'email')
|
if (prop === 'email')
|
||||||
isEmailValid.value = isValid
|
isEmailValid.value = isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendEmailCode() {
|
function sendEmailCode() {
|
||||||
emailFormRef.value.validate(isValid => {
|
emailFormRef.value.validate(isValid => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
@ -102,6 +105,7 @@ function sendEmailCode() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifyEmail() {
|
function modifyEmail() {
|
||||||
emailFormRef.value.validate(isValid => {
|
emailFormRef.value.validate(isValid => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
@ -113,6 +117,22 @@ function modifyEmail() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -120,7 +140,8 @@ function modifyEmail() {
|
|||||||
<div class="settings-left">
|
<div class="settings-left">
|
||||||
<card :icon="User" title="账号信息设置" desc="在这里编辑您的个人信息,您可以在隐私设置中选择是否展示这些信息"
|
<card :icon="User" title="账号信息设置" desc="在这里编辑您的个人信息,您可以在隐私设置中选择是否展示这些信息"
|
||||||
v-loading="loading.form">
|
v-loading="loading.form">
|
||||||
<el-form :model="baseForm" :rules="rules" ref="baseFormRef" label-position="top" style="margin: 0 10px 10px 10px">
|
<el-form :model="baseForm" :rules="rules" ref="baseFormRef" label-position="top"
|
||||||
|
style="margin: 0 10px 10px 10px">
|
||||||
<el-form-item label="用户名" prop="username">
|
<el-form-item label="用户名" prop="username">
|
||||||
<el-input v-model="baseForm.username" maxlength="10"/>
|
<el-input v-model="baseForm.username" maxlength="10"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -144,12 +165,15 @@ function modifyEmail() {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div>
|
<div>
|
||||||
<el-button :icon="Select" @click="saveDetails" :loading="loading.base"
|
<el-button :icon="Select" @click="saveDetails" :loading="loading.base"
|
||||||
type="success">保存用户信息</el-button>
|
type="success">保存用户信息
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</card>
|
</card>
|
||||||
<card style="margin-top: 10px" :icon="Message" title="电子邮件设置" desc="您可以在这里修改默认绑定的电子邮件地址">
|
<card style="margin-top: 10px" :icon="Message" title="电子邮件设置"
|
||||||
<el-form :rules="rules" @validate="onValidate" :model="emailForm" ref="emailFormRef" label-position="top" style="margin: 0 10px 10px 10px">
|
desc="您可以在这里修改默认绑定的电子邮件地址">
|
||||||
|
<el-form :rules="rules" @validate="onValidate" :model="emailForm" ref="emailFormRef"
|
||||||
|
label-position="top" style="margin: 0 10px 10px 10px">
|
||||||
<el-form-item label="电子邮件" prop="email">
|
<el-form-item label="电子邮件" prop="email">
|
||||||
<el-input v-model="emailForm.email"/>
|
<el-input v-model="emailForm.email"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -176,7 +200,17 @@ function modifyEmail() {
|
|||||||
<div style="position: sticky;top: 20px">
|
<div style="position: sticky;top: 20px">
|
||||||
<card>
|
<card>
|
||||||
<div style="text-align: center;padding: 5px 15px 0 15px">
|
<div style="text-align: center;padding: 5px 15px 0 15px">
|
||||||
<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 style="font-weight: bold">你好, {{ store.user.username }}</div>
|
||||||
</div>
|
</div>
|
||||||
<el-divider style="margin: 10px 0"/>
|
<el-divider style="margin: 10px 0"/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user