From ca442c7e678777e056e83add3e41aed2da12d632 Mon Sep 17 00:00:00 2001 From: NagoColer Date: Wed, 20 Sep 2023 01:39:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=A4=B4=E5=83=8F=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=9F=BA=E4=BA=8EMinio?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- my-project-backend/pom.xml | 6 ++ .../example/config/MinioConfiguration.java | 29 +++++++++ .../example/config/SecurityConfiguration.java | 1 + .../example/controller/ImageController.java | 35 ++++++++++ .../example/controller/ObjectController.java | 43 ++++++++++++ .../java/com/example/entity/dto/Account.java | 1 + .../example/entity/vo/response/AccountVO.java | 1 + .../com/example/filter/RequestLogFilter.java | 2 +- .../com/example/service/ImageService.java | 11 ++++ .../service/impl/AccountServiceImpl.java | 2 +- .../service/impl/ImageServiceImpl.java | 65 +++++++++++++++++++ .../src/main/resources/application-dev.yml | 4 ++ .../src/main/resources/application-prod.yml | 4 ++ my-project-frontend/src/net/index.js | 2 +- my-project-frontend/src/store/index.js | 11 +++- my-project-frontend/src/views/IndexView.vue | 2 +- .../src/views/forum/TopicList.vue | 2 +- .../src/views/settings/UserSetting.vue | 32 ++++++++- 18 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 my-project-backend/src/main/java/com/example/config/MinioConfiguration.java create mode 100644 my-project-backend/src/main/java/com/example/controller/ImageController.java create mode 100644 my-project-backend/src/main/java/com/example/controller/ObjectController.java create mode 100644 my-project-backend/src/main/java/com/example/service/ImageService.java create mode 100644 my-project-backend/src/main/java/com/example/service/impl/ImageServiceImpl.java diff --git a/my-project-backend/pom.xml b/my-project-backend/pom.xml index 085e7e8..6ec311a 100644 --- a/my-project-backend/pom.xml +++ b/my-project-backend/pom.xml @@ -94,6 +94,12 @@ springdoc-openapi-starter-webmvc-ui 2.1.0 + + + io.minio + minio + 8.3.9 + diff --git a/my-project-backend/src/main/java/com/example/config/MinioConfiguration.java b/my-project-backend/src/main/java/com/example/config/MinioConfiguration.java new file mode 100644 index 0000000..8aa9904 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/config/MinioConfiguration.java @@ -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(); + } +} diff --git a/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java b/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java index e61d41d..2db8779 100644 --- a/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java +++ b/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java @@ -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) ) diff --git a/my-project-backend/src/main/java/com/example/controller/ImageController.java b/my-project-backend/src/main/java/com/example/controller/ImageController.java new file mode 100644 index 0000000..1e814cd --- /dev/null +++ b/my-project-backend/src/main/java/com/example/controller/ImageController.java @@ -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 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, "头像上传失败!"); + } + } +} diff --git a/my-project-backend/src/main/java/com/example/controller/ObjectController.java b/my-project-backend/src/main/java/com/example/controller/ObjectController.java new file mode 100644 index 0000000..d267141 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/controller/ObjectController.java @@ -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; + } + } + } + } +} diff --git a/my-project-backend/src/main/java/com/example/entity/dto/Account.java b/my-project-backend/src/main/java/com/example/entity/dto/Account.java index 3fbfa1f..9d3729c 100644 --- a/my-project-backend/src/main/java/com/example/entity/dto/Account.java +++ b/my-project-backend/src/main/java/com/example/entity/dto/Account.java @@ -22,5 +22,6 @@ public class Account implements BaseData { String password; String email; String role; + String avatar; Date registerTime; } diff --git a/my-project-backend/src/main/java/com/example/entity/vo/response/AccountVO.java b/my-project-backend/src/main/java/com/example/entity/vo/response/AccountVO.java index ca187f1..8c4169e 100644 --- a/my-project-backend/src/main/java/com/example/entity/vo/response/AccountVO.java +++ b/my-project-backend/src/main/java/com/example/entity/vo/response/AccountVO.java @@ -9,5 +9,6 @@ public class AccountVO { String username; String email; String role; + String avatar; Date registerTime; } diff --git a/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java b/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java index 4ab82b6..4ef1199 100644 --- a/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java +++ b/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java @@ -29,7 +29,7 @@ public class RequestLogFilter extends OncePerRequestFilter { @Resource SnowflakeIdGenerator generator; - private final Set ignores = Set.of("/swagger-ui", "/v3/api-docs"); + private final Set ignores = Set.of("/swagger-ui", "/v3/api-docs", "/images"); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/my-project-backend/src/main/java/com/example/service/ImageService.java b/my-project-backend/src/main/java/com/example/service/ImageService.java new file mode 100644 index 0000000..1f1fd6b --- /dev/null +++ b/my-project-backend/src/main/java/com/example/service/ImageService.java @@ -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; +} diff --git a/my-project-backend/src/main/java/com/example/service/impl/AccountServiceImpl.java b/my-project-backend/src/main/java/com/example/service/impl/AccountServiceImpl.java index 642f277..288177f 100644 --- a/my-project-backend/src/main/java/com/example/service/impl/AccountServiceImpl.java +++ b/my-project-backend/src/main/java/com/example/service/impl/AccountServiceImpl.java @@ -99,7 +99,7 @@ public class AccountServiceImpl extends ServiceImpl 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 { diff --git a/my-project-backend/src/main/java/com/example/service/impl/ImageServiceImpl.java b/my-project-backend/src/main/java/com/example/service/impl/ImageServiceImpl.java new file mode 100644 index 0000000..ebdf655 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/service/impl/ImageServiceImpl.java @@ -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.update() + .eq("id", id) + .set("avatar", imageName)) > 0) { + return imageName; + } else { + return null; + } + } catch (Exception e) { + log.error("图片上传出现问题: "+e.getMessage(), e); + return null; + } + } +} diff --git a/my-project-backend/src/main/resources/application-dev.yml b/my-project-backend/src/main/resources/application-dev.yml index 443104b..358fa6d 100644 --- a/my-project-backend/src/main/resources/application-dev.yml +++ b/my-project-backend/src/main/resources/application-dev.yml @@ -39,3 +39,7 @@ spring: origin: '*' credentials: false methods: '*' + minio: + endpoint: 'http://localhost:9000' + username: 'minio' + password: 'password' diff --git a/my-project-backend/src/main/resources/application-prod.yml b/my-project-backend/src/main/resources/application-prod.yml index 984d8f4..4498287 100644 --- a/my-project-backend/src/main/resources/application-prod.yml +++ b/my-project-backend/src/main/resources/application-prod.yml @@ -43,3 +43,7 @@ spring: origin: '*' credentials: false methods: '*' + minio: + endpoint: 'http://localhost:9000' + username: 'minio' + password: 'password' diff --git a/my-project-frontend/src/net/index.js b/my-project-frontend/src/net/index.js index 29a4395..a790abb 100644 --- a/my-project-frontend/src/net/index.js +++ b/my-project-frontend/src/net/index.js @@ -99,4 +99,4 @@ function unauthorized() { return !takeAccessToken() } -export { post, get, login, logout, unauthorized } +export { post, get, login, logout, unauthorized, accessHeader } diff --git a/my-project-frontend/src/store/index.js b/my-project-frontend/src/store/index.js index 23a1201..de1ff23 100644 --- a/my-project-frontend/src/store/index.js +++ b/my-project-frontend/src/store/index.js @@ -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' + } } }) diff --git a/my-project-frontend/src/views/IndexView.vue b/my-project-frontend/src/views/IndexView.vue index f23fb15..277e9a1 100755 --- a/my-project-frontend/src/views/IndexView.vue +++ b/my-project-frontend/src/views/IndexView.vue @@ -59,7 +59,7 @@ function userLogout() {
{{store.user.email}}
- +