diff --git a/my-project-backend/pom.xml b/my-project-backend/pom.xml index 085e7e8..57211a5 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..3c568b2 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/config/MinioConfiguration.java @@ -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(); + } +} 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..c746109 --- /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() > 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, "头像上传失败,请联系管理员!"); + } + } +} 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..3573c38 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/controller/ObjectController.java @@ -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); + } + } + } + } +} 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..2e0ced9 --- /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(MultipartFile file, int id) throws IOException; + void fetchImageFromMinio(OutputStream stream, String image) 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 89b4f4c..700f875 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..05474f3 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/service/impl/ImageServiceImpl.java @@ -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.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 c3fb294..0649710 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 8f66d44..bdbd510 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 40217ac..c678217 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: '', + avatar: 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' + } } }) diff --git a/my-project-frontend/src/views/IndexView.vue b/my-project-frontend/src/views/IndexView.vue index cce2b5a..20c7591 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}}
- +