diff --git a/.gitignore b/.gitignore index 5c08283..31eae03 100755 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ build/ /han-note-auth/logs/ /logs/ /.idea/ +/han-note-oss/han-note-oss-biz/src/main/resources/application-dev.yml diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 7932393..8126366 100755 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -5,6 +5,12 @@ + + + + + + diff --git a/han-note-auth/pom.xml b/han-note-auth/pom.xml index 7f37689..c119563 100755 --- a/han-note-auth/pom.xml +++ b/han-note-auth/pom.xml @@ -97,6 +97,11 @@ com.hanserwei hanserwei-spring-boot-starter-biz-context + + + org.springframework.security + spring-security-crypto + diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/PasswordEncoderConfig.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/PasswordEncoderConfig.java new file mode 100644 index 0000000..3f349ee --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/PasswordEncoderConfig.java @@ -0,0 +1,21 @@ +package com.hanserwei.hannote.auth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + // BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。 + return new BCryptPasswordEncoder(); + } + + public static void main(String[] args) { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + System.out.println(encoder.encode("qwe123")); + } +} \ No newline at end of file diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/UserController.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/UserController.java index 3ca8fcc..e00621c 100644 --- a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/UserController.java +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/UserController.java @@ -2,13 +2,17 @@ package com.hanserwei.hannote.auth.controller; import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog; import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.auth.model.vo.user.UpdatePasswordReqVO; import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO; import com.hanserwei.hannote.auth.service.UserService; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") @@ -30,4 +34,10 @@ public class UserController { public Response logout() { return userService.logout(); } + + @PostMapping("/password/update") + @ApiOperationLog(description = "修改密码") + public Response updatePassword(@Validated @RequestBody UpdatePasswordReqVO updatePasswordReqVO) { + return userService.updatePassword(updatePasswordReqVO); + } } diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java index 4911adf..3caf502 100644 --- a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java @@ -15,6 +15,9 @@ public enum ResponseCodeEnum implements BaseExceptionInterface { // ----------- 业务异常状态码 ----------- VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试"), VERIFICATION_CODE_ERROR("AUTH-20001", "验证码错误"), + LOGIN_TYPE_ERROR("AUTH-20002", "登录类型错误"), + USER_NOT_FOUND("AUTH-20003", "该用户不存在"), + MAIL_OR_PASSWORD_ERROR("AUTH-20004", "邮箱号或密码错误"), diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/model/vo/user/UpdatePasswordReqVO.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/model/vo/user/UpdatePasswordReqVO.java new file mode 100644 index 0000000..caf0c70 --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/model/vo/user/UpdatePasswordReqVO.java @@ -0,0 +1,18 @@ +package com.hanserwei.hannote.auth.model.vo.user; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UpdatePasswordReqVO { + + @NotBlank(message = "新密码不能为空") + private String newPassword; + +} \ No newline at end of file diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/UserService.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/UserService.java index 40b9a48..71932e6 100644 --- a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/UserService.java +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/UserService.java @@ -3,6 +3,7 @@ package com.hanserwei.hannote.auth.service; import com.baomidou.mybatisplus.extension.service.IService; import com.hanserwei.framework.common.response.Response; import com.hanserwei.hannote.auth.domain.dataobject.UserDO; +import com.hanserwei.hannote.auth.model.vo.user.UpdatePasswordReqVO; import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO; public interface UserService extends IService { @@ -20,4 +21,11 @@ public interface UserService extends IService { * @return 响应结果 */ Response logout(); + + /** + * 修改密码 + * @param updatePasswordReqVO 请求参数 + * @return 响应结果 + */ + Response updatePassword(UpdatePasswordReqVO updatePasswordReqVO); } diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/UserServiceImpl.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/UserServiceImpl.java index db00b49..c41553c 100644 --- a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/UserServiceImpl.java +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/UserServiceImpl.java @@ -22,6 +22,7 @@ import com.hanserwei.hannote.auth.domain.mapper.UserDOMapper; import com.hanserwei.hannote.auth.domain.mapper.UserRoleDOMapper; import com.hanserwei.hannote.auth.enums.LoginTypeEnum; import com.hanserwei.hannote.auth.enums.ResponseCodeEnum; +import com.hanserwei.hannote.auth.model.vo.user.UpdatePasswordReqVO; import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO; import com.hanserwei.hannote.auth.service.UserService; import jakarta.annotation.Resource; @@ -30,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; @@ -50,6 +52,7 @@ public class UserServiceImpl extends ServiceImpl implement private final RoleDOMapper roleDOMapper; @Resource(name = "authTaskExecutor") private ThreadPoolTaskExecutor authTaskExecutor; + private final PasswordEncoder passwordEncoder; @Override public Response loginAndRegister(UserLoginReqVO reqVO) { @@ -87,6 +90,19 @@ public class UserServiceImpl extends ServiceImpl implement } break; case PASSWORD: + String password = reqVO.getPassword(); + // 根据邮箱号查询 + UserDO userDO1 = this.getOne(new QueryWrapper().eq("email", email)); + if (Objects.isNull(userDO1)){ + throw new ApiException(ResponseCodeEnum.USER_NOT_FOUND); + } + // 拿到密文密码 + String encodePassword = userDO1.getPassword(); + boolean isPasswordCorrect = passwordEncoder.matches(password, encodePassword); + if (!isPasswordCorrect) { + throw new ApiException(ResponseCodeEnum.MAIL_OR_PASSWORD_ERROR); + } + userId = userDO1.getId(); break; default: break; @@ -158,4 +174,23 @@ public class UserServiceImpl extends ServiceImpl implement StpUtil.logout(userId); return Response.success(); } + + @Override + public Response updatePassword(UpdatePasswordReqVO updatePasswordReqVO) { + // 新密码 + String newPassword = updatePasswordReqVO.getNewPassword(); + // 加密后的密码 + String encodePassword = passwordEncoder.encode(newPassword); + // 获取用户ID + Long userId = LoginUserContextHolder.getUserId(); + + UserDO userDO = UserDO.builder() + .id(userId) + .password(encodePassword) + .updateTime(LocalDateTime.now()) + .build(); + // 更新用户密码 + this.updateById(userDO); + return Response.success(); + } } diff --git a/han-note-oss/han-note-oss-api/pom.xml b/han-note-oss/han-note-oss-api/pom.xml new file mode 100644 index 0000000..d162cf2 --- /dev/null +++ b/han-note-oss/han-note-oss-api/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + + com.hanserwei + han-note-oss + ${revision} + + + + jar + + han-note-oss-api + ${project.artifactId} + RPC层, 供其他服务调用 + + + + com.hanserwei + hanserwei-common + + + diff --git a/han-note-oss/han-note-oss-biz/pom.xml b/han-note-oss/han-note-oss-biz/pom.xml new file mode 100644 index 0000000..8d011fd --- /dev/null +++ b/han-note-oss/han-note-oss-biz/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + + + com.hanserwei + han-note-oss + ${revision} + + + + jar + + han-note-oss-biz + ${project.artifactId} + 对象存储业务层 + + + + com.hanserwei + hanserwei-common + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + software.amazon.awssdk + s3 + + + + com.aliyun.oss + aliyun-sdk-oss + + + + javax.xml.bind + jaxb-api + + + javax.activation + activation + + + + org.glassfish.jaxb + jaxb-runtime + + + + com.qcloud + cos_api + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/HannoteOssBizApplication.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/HannoteOssBizApplication.java new file mode 100644 index 0000000..b66c51e --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/HannoteOssBizApplication.java @@ -0,0 +1,12 @@ +package com.hanserwei.hannote.oss; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HannoteOssBizApplication { + public static void main(String[] args) { + SpringApplication.run(HannoteOssBizApplication.class, args); + } +} diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/AliyunOSSConfig.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/AliyunOSSConfig.java new file mode 100644 index 0000000..ebdb7aa --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/AliyunOSSConfig.java @@ -0,0 +1,31 @@ +package com.hanserwei.hannote.oss.config; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.common.auth.CredentialsProviderFactory; +import com.aliyun.oss.common.auth.DefaultCredentialProvider; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AliyunOSSConfig { + + @Resource + private AliyunOSSProperties aliyunOSSProperties; + + /** + * 构建 阿里云 OSS 客户端 + * + * @return 阿里云 OSS 客户端 + */ + @Bean + public OSS aliyunOSSClient() { + // 设置访问凭证 + DefaultCredentialProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider( + aliyunOSSProperties.getAccessKey(), aliyunOSSProperties.getSecretKey()); + + // 创建 OSSClient 实例 + return new OSSClientBuilder().build(aliyunOSSProperties.getEndpoint(), credentialsProvider); + } +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/AliyunOSSProperties.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/AliyunOSSProperties.java new file mode 100644 index 0000000..c0cc8cf --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/AliyunOSSProperties.java @@ -0,0 +1,14 @@ +package com.hanserwei.hannote.oss.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "storage.aliyun-oss") +@Component +@Data +public class AliyunOSSProperties { + private String endpoint; + private String accessKey; + private String secretKey; +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/CosConfig.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/CosConfig.java new file mode 100644 index 0000000..27e5f7a --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/CosConfig.java @@ -0,0 +1,48 @@ +package com.hanserwei.hannote.oss.config; + +import com.qcloud.cos.COSClient; +import com.qcloud.cos.ClientConfig; +import com.qcloud.cos.auth.BasicCOSCredentials; +import com.qcloud.cos.auth.COSCredentials; +import com.qcloud.cos.endpoint.EndpointBuilder; +import com.qcloud.cos.region.Region; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CosConfig { + + @Resource + private CosProperties cosProperties; + + @Bean + public COSClient cosClient() { + // 1. 初始化用户身份信息(SecretId, SecretKey) + COSCredentials cred = new BasicCOSCredentials( + cosProperties.getSecretId(), + cosProperties.getSecretKey() + ); + + // 2. 设置 bucket 的地域 + Region region = new Region(cosProperties.getRegion()); + ClientConfig clientConfig = new ClientConfig(region); + if (cosProperties.getEndpoint() != null && !cosProperties.getEndpoint().isEmpty()) { + clientConfig.setEndpointBuilder(new EndpointBuilder() { + @Override + public String buildGeneralApiEndpoint(String bucketName) { + // 所有 API 请求都会使用自定义域名 + return cosProperties.getEndpoint(); + } + + @Override + public String buildGetServiceApiEndpoint() { + return cosProperties.getEndpoint(); + } + }); + } + + // 3. 构建 COSClient + return new COSClient(cred, clientConfig); + } +} diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/CosProperties.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/CosProperties.java new file mode 100644 index 0000000..a9cc9f1 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/CosProperties.java @@ -0,0 +1,16 @@ +package com.hanserwei.hannote.oss.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Data +@ConfigurationProperties(prefix = "storage.cos") +public class CosProperties { + private String endpoint; + private String secretId; + private String secretKey; + private String appId; + private String region; +} diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/RustfsConfig.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/RustfsConfig.java new file mode 100644 index 0000000..469f387 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/RustfsConfig.java @@ -0,0 +1,28 @@ +package com.hanserwei.hannote.oss.config; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; + +@Configuration +public class RustfsConfig { + + @Resource + private RustfsProperties rustfsProperties; + + @Bean + public S3Client minioClient() { + // 构建 Rustfs 客户端 + return S3Client.builder() + .endpointOverride(URI.create(rustfsProperties.getEndpoint())) + .region(Region.US_EAST_1) + .credentialsProvider(() -> AwsBasicCredentials.create(rustfsProperties.getAccessKey(), rustfsProperties.getSecretKey())) + .forcePathStyle(true) + .build(); + } +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/RustfsProperties.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/RustfsProperties.java new file mode 100644 index 0000000..7284e0b --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/config/RustfsProperties.java @@ -0,0 +1,14 @@ +package com.hanserwei.hannote.oss.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "storage.rustfs") +@Component +@Data +public class RustfsProperties { + private String endpoint; + private String accessKey; + private String secretKey; +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/controller/FileController.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/controller/FileController.java new file mode 100644 index 0000000..54748cb --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/controller/FileController.java @@ -0,0 +1,27 @@ +package com.hanserwei.hannote.oss.controller; + +import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.oss.service.FileService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/file") +@Slf4j +public class FileController { + + @Resource + private FileService fileService; + + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public Response uploadFile(@RequestPart(value = "file") MultipartFile file) { + return fileService.uploadFile(file); + } + +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/enums/ResponseCodeEnum.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/enums/ResponseCodeEnum.java new file mode 100644 index 0000000..98bebb0 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/enums/ResponseCodeEnum.java @@ -0,0 +1,23 @@ +package com.hanserwei.hannote.oss.enums; + +import com.hanserwei.framework.common.exception.BaseExceptionInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ResponseCodeEnum implements BaseExceptionInterface { + + // ----------- 通用异常状态码 ----------- + SYSTEM_ERROR("OSS-10000", "出错啦,后台小维正在努力修复中..."), + PARAM_NOT_VALID("OSS-10001", "参数错误!!!"), + + // ----------- 业务异常状态码 ----------- + ; + + // 异常码 + private final String errorCode; + // 错误信息 + private final String errorMsg; + +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/exception/GlobalExceptionHandler.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..0d45e60 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/exception/GlobalExceptionHandler.java @@ -0,0 +1,103 @@ +package com.hanserwei.hannote.oss.exception; + +import com.hanserwei.framework.common.exception.ApiException; +import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.oss.enums.ResponseCodeEnum; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Optional; + +@SuppressWarnings("LoggingSimilarMessage") +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 捕获自定义业务异常 + * + * @return Response.fail(e) + */ + @ExceptionHandler({ApiException.class}) + @ResponseBody + public Response handleApiException(HttpServletRequest request, ApiException e) { + log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMsg()); + return Response.fail(e); + } + + /** + * 捕获参数校验异常 + * + * @return Response.fail(errorCode, errorMessage) + */ + @ExceptionHandler({MethodArgumentNotValidException.class}) + @ResponseBody + public Response handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) { + // 参数错误异常码 + String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(); + + // 获取 BindingResult + BindingResult bindingResult = e.getBindingResult(); + + StringBuilder sb = new StringBuilder(); + + // 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com'; + Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> { + errors.forEach(error -> + sb.append(error.getField()) + .append(" ") + .append(error.getDefaultMessage()) + .append(", 当前值: '") + .append(error.getRejectedValue()) + .append("'; ") + + ); + }); + + // 错误信息 + String errorMessage = sb.toString(); + + log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage); + + return Response.fail(errorCode, errorMessage); + } + + /** + * 捕获 guava 参数校验异常 + * + * @return Response.fail(ResponseCodeEnum.PARAM_NOT_VALID) + */ + @ExceptionHandler({IllegalArgumentException.class}) + @ResponseBody + public Response handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) { + // 参数错误异常码 + String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(); + + // 错误信息 + String errorMessage = e.getMessage(); + + log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage); + + return Response.fail(errorCode, errorMessage); + } + + /** + * 其他类型异常 + * + * @param request 请求 + * @param e 异常 + * @return Response.fail(ResponseCodeEnum.SYSTEM_ERROR) + */ + @ExceptionHandler({Exception.class}) + @ResponseBody + public Response handleOtherException(HttpServletRequest request, Exception e) { + log.error("{} request error, ", request.getRequestURI(), e); + return Response.fail(ResponseCodeEnum.SYSTEM_ERROR); + } +} + diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/factory/FileStrategyFactory.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/factory/FileStrategyFactory.java new file mode 100644 index 0000000..247cf92 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/factory/FileStrategyFactory.java @@ -0,0 +1,34 @@ +package com.hanserwei.hannote.oss.factory; + +import com.hanserwei.hannote.oss.strategy.FileStrategy; +import com.hanserwei.hannote.oss.strategy.impl.AliyunOSSFileStrategy; +import com.hanserwei.hannote.oss.strategy.impl.CosFileStrategy; +import com.hanserwei.hannote.oss.strategy.impl.RustfsFileStrategy; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RefreshScope +public class FileStrategyFactory { + + @Value("${storage.type}") + private String strategyType; + + @Bean + @RefreshScope + public FileStrategy getFileStrategy() { + if (strategyType == null) { + throw new IllegalArgumentException("存储类型不能为空"); + } + + return switch (strategyType.toLowerCase()) { + case "rustfs" -> new RustfsFileStrategy(); + case "aliyun" -> new AliyunOSSFileStrategy(); + case "cos" -> new CosFileStrategy(); + default -> throw new IllegalArgumentException("不可用的存储类型: " + strategyType); + }; + } + +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/service/FileService.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/service/FileService.java new file mode 100644 index 0000000..73e29b2 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/service/FileService.java @@ -0,0 +1,15 @@ +package com.hanserwei.hannote.oss.service; + +import com.hanserwei.framework.common.response.Response; +import org.springframework.web.multipart.MultipartFile; + +public interface FileService { + + /** + * 上传文件 + * + * @param file 文件 + * @return 文件上传结果 + */ + Response uploadFile(MultipartFile file); +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/service/impl/FileServiceImpl.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..e6f24b8 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/service/impl/FileServiceImpl.java @@ -0,0 +1,25 @@ +package com.hanserwei.hannote.oss.service.impl; + +import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.oss.service.FileService; +import com.hanserwei.hannote.oss.strategy.FileStrategy; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Service +public class FileServiceImpl implements FileService { + @Resource + private FileStrategy fileStrategy; + + private static final String BUCKET_NAME = "han-note"; + + @Override + public Response uploadFile(MultipartFile file) { + // 上传文件 + String url = fileStrategy.uploadFile(file, BUCKET_NAME); + return Response.success(url); + } +} diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/FileStrategy.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/FileStrategy.java new file mode 100644 index 0000000..f0c099e --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/FileStrategy.java @@ -0,0 +1,16 @@ +package com.hanserwei.hannote.oss.strategy; + +import org.springframework.web.multipart.MultipartFile; + +public interface FileStrategy { + + /** + * 文件上传 + * + * @param file 文件 + * @param bucketName 存储桶名称 + * @return 文件访问路径 + */ + String uploadFile(MultipartFile file, String bucketName); + +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/AliyunOSSFileStrategy.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/AliyunOSSFileStrategy.java new file mode 100644 index 0000000..8ba798e --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/AliyunOSSFileStrategy.java @@ -0,0 +1,58 @@ +package com.hanserwei.hannote.oss.strategy.impl; + +import com.aliyun.oss.OSS; +import com.hanserwei.hannote.oss.config.AliyunOSSProperties; +import com.hanserwei.hannote.oss.strategy.FileStrategy; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.util.UUID; + +@Slf4j +public class AliyunOSSFileStrategy implements FileStrategy { + + @Resource + private AliyunOSSProperties aliyunOSSProperties; + + @Resource + private OSS ossClient; + + @Override + @SneakyThrows + public String uploadFile(MultipartFile file, String bucketName) { + log.info("## 上传文件至阿里云 OSS ..."); + + // 判断文件是否为空 + if (file == null || file.getSize() == 0) { + log.error("==> 上传文件异常:文件大小为空 ..."); + throw new RuntimeException("文件大小不能为空"); + } + + // 文件的原始名称 + String originalFileName = file.getOriginalFilename(); + + // 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串) + String key = UUID.randomUUID().toString().replace("-", ""); + // 获取文件的后缀,如 .jpg + String suffix = null; + if (originalFileName != null) { + suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + } + + // 拼接上文件后缀,即为要存储的文件名 + String objectName = String.format("%s%s", key, suffix); + + log.info("==> 开始上传文件至阿里云 OSS, ObjectName: {}", objectName); + + // 上传文件至阿里云 OSS + ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(file.getInputStream().readAllBytes())); + + // 返回文件的访问链接 + String url = String.format("https://%s.%s/%s", bucketName, aliyunOSSProperties.getEndpoint(), objectName); + log.info("==> 上传文件至阿里云 OSS 成功,访问路径: {}", url); + return url; + } +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/CosFileStrategy.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/CosFileStrategy.java new file mode 100644 index 0000000..f86ff49 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/CosFileStrategy.java @@ -0,0 +1,65 @@ +package com.hanserwei.hannote.oss.strategy.impl; + +import com.hanserwei.hannote.oss.config.CosProperties; +import com.hanserwei.hannote.oss.strategy.FileStrategy; +import com.qcloud.cos.COSClient; +import com.qcloud.cos.model.ObjectMetadata; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.UUID; + +@SuppressWarnings("DuplicatedCode") +@Slf4j +public class CosFileStrategy implements FileStrategy { + @Resource + private CosProperties cosProperties; + @Resource + private COSClient cosClient; + + @Override + @SneakyThrows + public String uploadFile(MultipartFile file, String bucketName) { + log.info("## 上传文件至腾讯云Cos ..."); + + // 判断文件是否为空 + if (file == null || file.getSize() == 0) { + log.error("==> 上传文件异常:文件大小为空 ..."); + throw new RuntimeException("文件大小不能为空"); + } + + // 文件的原始名称 + String originalFileName = file.getOriginalFilename(); + + // 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串) + String key = UUID.randomUUID().toString().replace("-", ""); + // 获取文件的后缀,如 .jpg + String suffix = null; + if (originalFileName != null) { + suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + } + + // 拼接上文件后缀,即为要存储的文件名 + String objectName = String.format("%s%s", key, suffix); + + log.info("==> 开始上传文件至腾讯云Cos, ObjectName: {}", objectName); + + // 设置元数据 + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); + + // 执行上传 + try (InputStream inputStream = file.getInputStream()) { + cosClient.putObject(bucketName, objectName, inputStream, metadata); + } + + // 返回文件的访问链接 + String url = String.format("https://%s/%s", cosProperties.getEndpoint(), objectName); + log.info("==> 上传文件至腾讯云 Cos 成功,访问路径: {}", url); + return url; + } +} diff --git a/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/RustfsFileStrategy.java b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/RustfsFileStrategy.java new file mode 100644 index 0000000..d79b322 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/java/com/hanserwei/hannote/oss/strategy/impl/RustfsFileStrategy.java @@ -0,0 +1,73 @@ +package com.hanserwei.hannote.oss.strategy.impl; + +import com.hanserwei.hannote.oss.config.RustfsProperties; +import com.hanserwei.hannote.oss.strategy.FileStrategy; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.InputStream; +import java.util.UUID; + +@Slf4j +public class RustfsFileStrategy implements FileStrategy { + + @Resource + private RustfsProperties rustfsProperties; + + @Resource + private S3Client rustfsClient; + + @Override + @SneakyThrows + public String uploadFile(MultipartFile file, String bucketName) { + log.info("## 上传文件至 Rustfs ..."); + + // 判断文件是否为空 + if (file == null || file.isEmpty()) { + log.error("==> 上传文件异常:文件为空 ..."); + throw new RuntimeException("文件不能为空"); + } + + // 文件的原始名称 + String originalFileName = file.getOriginalFilename(); + String contentType = file.getContentType(); + + // 生成存储对象的名称 + String key = UUID.randomUUID().toString().replace("-", ""); + String suffix = ""; + if (originalFileName != null && originalFileName.contains(".")) { + suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + } + + // 拼接最终文件名 + String objectName = key + suffix; + + log.info("==> 开始上传文件至 Rustfs, ObjectName: {}", objectName); + + // 执行上传 + try (InputStream inputStream = file.getInputStream()) { + rustfsClient.putObject( + PutObjectRequest.builder() + .bucket(bucketName) // 方法参数传入 bucketName + .key(objectName) + .contentType(contentType) + .build(), + RequestBody.fromInputStream(inputStream, file.getSize()) + ); + } + + // 返回文件的访问链接(注意:Rustfs 是否支持直链要看配置) + String url = String.format("%s/%s/%s", + rustfsProperties.getEndpoint().replaceAll("/$", ""), // 去掉结尾的斜杠 + bucketName, + objectName); + + log.info("==> 上传文件至 Rustfs 成功,访问路径: {}", url); + return url; + } +} \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/resources/application.yml b/han-note-oss/han-note-oss-biz/src/main/resources/application.yml new file mode 100644 index 0000000..a814e02 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/resources/application.yml @@ -0,0 +1,9 @@ +server: + port: 8081 # 项目启动的端口 + +spring: + profiles: + active: dev # 默认激活 dev 本地开发环境 + +storage: + type: rustfs # 对象存储类型 \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/resources/bootstrap.yml b/han-note-oss/han-note-oss-biz/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..c969957 --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/resources/bootstrap.yml @@ -0,0 +1,19 @@ +spring: + application: + name: han-note-oss # 应用名称 + profiles: + active: dev # 默认激活 dev 本地开发环境 + cloud: + nacos: + discovery: + enabled: true # 启用服务发现 + group: DEFAULT_GROUP # 所属组 + namespace: han-note # 命名空间 + server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址 + config: + server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址 + prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀 + group: DEFAULT_GROUP # 所属组 + namespace: han-note # 命名空间 + file-extension: yaml # 配置文件格式 + refresh-enabled: true # 是否开启动态刷新 \ No newline at end of file diff --git a/han-note-oss/han-note-oss-biz/src/main/resources/logback-spring.xml b/han-note-oss/han-note-oss-biz/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..72c56ef --- /dev/null +++ b/han-note-oss/han-note-oss-biz/src/main/resources/logback-spring.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + ${LOG_FILE}-%i.log + + 30 + + 10MB + + 0 + + false + + + ${LOG_PATTERN} + UTF-8 + + + + + + + 0 + + 256 + + + + + + + + + + + + + + + + + + + + + diff --git a/han-note-oss/pom.xml b/han-note-oss/pom.xml new file mode 100644 index 0000000..0cd8efa --- /dev/null +++ b/han-note-oss/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + + com.hanserwei + han-note + ${revision} + + + + pom + + + + han-note-oss-api + han-note-oss-biz + + + han-note-oss + + ${project.artifactId} + + 对象存储服务 + diff --git a/pom.xml b/pom.xml index ef8700b..4226bd7 100755 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ han-note-auth hanserwei-framework han-note-gateway + han-note-oss @@ -42,6 +43,12 @@ 5.8.40 3.19.0 2.14.5 + 2.34.8 + 3.17.4 + 2.3.1 + 1.1.1 + 2.3.3 + 5.6.227 @@ -161,6 +168,40 @@ transmittable-thread-local ${transmittable-thread-local.version} + + software.amazon.awssdk + s3 + ${aws-sdk.version} + + + + com.aliyun.oss + aliyun-sdk-oss + ${aliyun-sdk-oss.version} + + + + javax.xml.bind + jaxb-api + ${jaxb-api.version} + + + javax.activation + activation + ${activation.version} + + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb-runtime.version} + + + + com.qcloud + cos_api + ${cos-api.version} +