万用模板
快来分享你的内容吧~
- 2025-03-13·算法工程师前言 没有学习用户中心,直接上手了OJ系统,对万用模板有点懵,于是把文件的架构自己列了一遍。 改天再补充完整对代码的解释。查看全文leikooo:有后端模板相关的解析 https://www.codefather.cn/course/1789200994552225793523分享
- 2025-03-11·Java后端
- 2024-09-13
- 2024-09-01·后端开发后端万用模板是在哪个项目中搭建的呀 想学一下搭建的思路 然后自己搭建一个属于自己的后端模板编程导航_小y:用户中心项目和伙伴匹配项目的前两期都讲解了,是在项目直播中整合出来的,没有单独来讲解。可以看看这个教程,里面有讲解万用模板的用法:https://www.codefather.cn/course/1789200994552225793/section/1789201346202673154?type=160分享
鱼皮后端通用模板学习 — 文件结构
#### 前言 没有学习用户中心,直接上手了OJ系统,对万用模板有点懵,于是把文件的架构自己列了一遍。 改天再补充完整对代码的解释。 #### 整体架构 - sql - create_table. sql 数据库初始化建表语句 > ```sql > -- 创建库 > create database if not exists yyzq_oj; > > -- 切换库 > use yyzq_oj; > > -- 用户表 > create table if not exists user > ( > id bigint auto_increment comment 'id' primary key, > userAccount varchar(256) not null comment '账号', > userPassword varchar(512) not null comment '密码', > unionId varchar(256) null comment '微信开放平台id', > mpOpenId varchar(256) null comment '公众号openId', > userName varchar(256) null comment '用户昵称', > userAvatar varchar(1024) null comment '用户头像', > userProfile varchar(512) null comment '用户简介', > userRole varchar(256) default 'user' not null comment '用户角色:user/admin/ban', > createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', > updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', > isDelete tinyint default 0 not null comment '是否删除', > index idx_unionId (unionId) > ) comment '用户' collate = utf8mb4_unicode_ci; > ``` - post_es_mapping. json > post_es_mapping. json 通常是用于向 Elasticsearch(ES)发送请求,以定义索引映射(mapping)的 JSON 文件。 > Elasticsearch 是基于 Lucene 的**分布式搜索引擎**,专为**实时搜索、日志分析、数据检索**而生,支持 PB 级(TB更高一级)数据的高速查询。 > > 介绍视频:[Elastic Search是什么?Lucene是什么?架构是怎么样的?_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1yb421J7oX/) > 配套文档:https://mp.weixin.qq.com/s/RUQXIyN95hvi2wM3CyPI9w - src - main - java - 包1(generator) - 包2(com. yyzq. yyzqOJ) - resources - test 包内架构概览 - annotation:**注解** - @AuthCheck :自定义注解,必须有某个角色才能使用。 > ```java > @AuthCheck(mustRole = "admin") > public void deleteUser() { > // 只有具备 "admin" 角色的用户才能调用该方法 > System.out.println("删除用户"); > } > ``` - aop:面向切面编程,扩充一些方法通用的功能段 - AuthInterceptor 登录和权限的校验 - LogInterceptor 请求日志捕获,会捕获所有 controller ,进行计时和路径记录。 - common: java 后端代码**常用**的包装类。 - BaseResponse:前后端交互的统一相应结构 code + message + data - DeleteRequest:对删除请求进行封装,十分简洁 - ErrorCode:枚举类,封装了很多错误信息,保证错误码的统一。 - PageRequest:分页请求参数,用于页面的分页显示 - ResultUtils:快速返回 BaseResponse 数据的包装类 - config:**配置**文件,Spring会在启动时自动扫描并加载该目录下文件(@Configuration) - CorsConfig:开启后实现跨域请求 - CosClientConfig:使用腾讯云对象存储客户端 对应 application.yml 中字段: ```yaml # todo 启动腾讯云存储,需替换配置 cos: client: accessKey: xxx secretKey: xxx region: xxx bucket: xxx ``` - JsonConfig: Spring MVC 自带的 Jackson,Long 转 json 精度丢失需要转字符串来修正。 - MyBatisPlusConfig:用于简化分页 - WxOpenConfig: 微信开放平台配置,从配置文件中读取微信开放平台的应用 ID 和应用密钥,然后采用单例模式创建并初始化一个`WxMpService`实例 配置文件: ```yaml wx: # 微信开放平台配置 # todo 使用微信开放平台,需替换配置 open: appId: xxx appSecret: xxx ``` - constant:一些**常量** - CommonConstant:通用常量,如升序,降序。 - FileConstant:文件常量,如腾讯云对象存储访问地址( COS ,Cloud Object Storage) - UserConstant:用户常量,包括用户的状态,如登录,管理员,被封号等。 ```java public interface UserConstant { // 用户登录态键 String USER_LOGIN_STATE = "user_login"; // region 权限 String DEFAULT_ROLE = "user"; String ADMIN_ROLE = "admin"; String BAN_ROLE = "ban"; } ``` - controller:三层架构中的表现层,负责接受用户的请求,并调用业务层的逻辑方法。 - FileController:文件接口,该控制器负责处理文件上传相关的请求 - PostController - PostFavourController - PostThumbController - UserController - WxMpController - esdao: Elasticsearch 相关代码 - PostEsDao 帖子 Es 相关操作,还没实现 - exception:异常处理类 - BusinessException:自定义异常类 - GlobalExceptionhandler:统一处理抛出的异常,避免异常直接暴露给用户,增强健壮性和用户体验。 - ThrowUtils:抛异常工具类,若条件成立则抛出异常 - generate: MybatisX 生成的代码存放的位置。 - job:定时任务和单次任务,取消注释 @Component 开启 - IncSyncPostToEs:将数据库中近 5 分钟内有更新的帖子数据增量同步到 Elasticsearch(ES)中。 - FullSyncPostToEs:将数据库中的所有帖子数据全量同步到 Elasticsearch(ES)中。 - manager:自己定义的中间层,在 Service 和 Dao 中间的一个层,通用但和业务无关。 - CosManager:将文件上传到腾讯云 - mapper:三层架构的数据访问层,对数据库的操作,由 MybatisX 生成,方法都在对应的 XML 中。 - model:数据类的封装 - dto:用于不同层之间的传输,避免直接依赖实体类,也可以对数据进行筛选,如各种 Request - entity:各种实体类,如 帖子,问题,用户 - enums:各种枚举类,如用户权限,OJ 判题语言等 - vo:后端返回给前端的数据,部分隐私数据不会给前端,如 OJ 中的题目用例 - service:三层架构的业务逻辑层,调用 Mapper 层与数据库交互。 - 若干接口,表示要实现的逻辑。 - impl 文件夹: implement,文件夹内是各种对接口的实现,以此选择注入的类。 - utils:自己写的工具类 - NetUtils - SpringContextUtils - SqlUtils - wxmp:微信公众号相关的开发代码 - MainApplication:项目启动入口
编程导航后端模板-登录功能改造
# 突发奇想 在学习OJ项目的时候,想着后期跟着教程学完后,想扩展更多的登录方式。想到后期代码偏多不如现在就直接扩展,于是就有了以下的操作。 # 模板中的登录接口 现有的登录接口 <img src="https://pic.code-nav.cn/post_picture/1738491648276455426/i5WfxQN82g834bml.png" alt="image.png" width="560px" /> 存在多个登录认证接口,如果又有别的登录方式,又要多写一个接口。显然是不合理的,那么我们需要设计成对外只提供一个认证接口,然后内部使用根据不同的请求类型,做不同的认证。 这里需要使用到 **策略模式** 和 SpringBoot中的Ioc来实现调用对应的策略 ## 认证接口设计 URI: auth/login 请求参数: | 参数名 | 参数说明 | | :---: | :---: | | authType | 认证类型;password, wx_open | | data | 认证参数(JSON):不同的authType有不同的请求参数 | ## UML 定义一个策略接口:AuthStrategy,用来管理系统的认证接口。 AuthStrategy: 1. 定义一个默认方法:用于根据不同的authType来调用系统中不同的具体策略类。 2. 提供一个抽象方法:用于给具体策略类来实现自己不同的登录策略 AuthPasswordStrategy、WxOpenStrategy就是具体的登录策略类,需要实现抽象策略类 ## 实现步骤 ### 第一步:前期准备 认证类型枚举类 ```java /** * 系统认证方式 */ @Getter public enum AuthTypeEnum { PASSWORD("password", "账号密码"), WX_OPEN("wx_open", "微信开放平台"); /** * 枚举值 */ final String value; /** * 描述 */ final String desc; AuthTypeEnum(String value, String desc) { this.value = value; this.desc = desc; } /** * 校验传入的value是否存在,如果存在就返回AuthTypeEnum枚举值,如果不存在就抛出异常 * * @param value 枚举值 * @return 枚举对象 */ public static AuthTypeEnum getEnumByValue(String value) { if (value == null || value.isEmpty()) { return null; } for (AuthTypeEnum anEnum : AuthTypeEnum.values()) { if (anEnum.value.equals(value)) { return anEnum; } } throw new BusinessException(ErrorCode.PARAMS_ERROR, "该认证方式系统暂未开放"); } } ``` 认证请求类 ```java /** * 系统认证请求类 */ @Data public class AuthRequest implements Serializable { /** * 认证类型 */ private String authType; /** * 认证数据 */ private String data; } ``` 修改模板中的SpringContextUtils ```java /** * Spring 上下文获取工具 */ @Component public class SpringContextUtils extends SpringUtil { /** * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true */ public static boolean containsBean(String name) { return getBeanFactory().containsBean(name); } /** * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) */ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { return getBeanFactory().isSingleton(name); } /** * @return Class 注册对象的类型 */ public static Class<?> getType(String name) throws NoSuchBeanDefinitionException { return getBeanFactory().getType(name); } /** * 如果给定的bean名字在bean定义中有别名,则返回这些别名 */ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { return getBeanFactory().getAliases(name); } /** * 获取aop代理对象 */ @SuppressWarnings("unchecked") public static <T> T getAopProxy(T invoker) { return (T) getBean(invoker.getClass()); } /** * 获取spring上下文 */ public static ApplicationContext context() { return getApplicationContext(); } } ``` ### 第二步:定义AuthStrategy抽象策略类 ```java /** * 登录认证抽象策略类 * 用于分发登录认证方式 */ public interface AuthStrategy { /** * 认证方式前缀 */ String AUTH_BASE_KEY = "auth:"; /** * 登录认证 * 该方法用于分发认证方式,并返回登录用户信息。 * 根据SpringContextUtils获取对应的认证方式,并调用对应的认证方法。 * * @param authRequest 认证请求 * @return 登录用户信息 {@link LoginUserVO} */ static LoginUserVO login(AuthRequest authRequest) { // 1. 校验authType AuthTypeEnum authTypeEnum = AuthTypeEnum.getEnumByValue(authRequest.getAuthType()); // 2. 使用SpringContextUtils获取对应的认证方式 String authBeanName = AUTH_BASE_KEY + authTypeEnum.getValue(); AuthStrategy authStrategy = SpringContextUtils.getBean(authBeanName); return authStrategy.login(authRequest.getData()); } /** * 抽象认证方法 * @param data 认证数据 Json * @return 登录用户信息 {@link LoginUserVO} */ LoginUserVO login(String data); } ``` ### 第三步:具体策略类实现 password具体策略类 ```java /** * 账号密码认证策略 * * @author sakura */ @Service(AuthStrategy.AUTH_BASE_KEY + "password") public class PasswordAuthStrategy implements AuthStrategy { @Resource private UserService userService; @Override public LoginUserVO login(String data) { // 1. 解析data 会出现解析异常 UserLoginRequest userLoginRequest = JSONUtil.toBean(data, UserLoginRequest.class); if (ObjectUtils.isEmpty(userLoginRequest)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数未传递"); } if (ObjectUtils.anyNull(userLoginRequest.getUserAccount(), userLoginRequest.getUserPassword())) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数未传递"); } // 2. 获取请求上下文 todo 如果后期使用redis缓存登录信息,可以将这里删除 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求上下文未找到"); } HttpServletRequest request = attributes.getRequest(); return userService.userLogin(userLoginRequest.getUserAccount(), userLoginRequest.getUserPassword(), request); } } ``` 微信开发平台 ```java /** * 微信开放平台认证策略 * * @author sakura */ @Slf4j @Service(AuthStrategy.AUTH_BASE_KEY + "wx_open") public class WxOpenAuthStrategy implements AuthStrategy { @Resource private WxOpenConfig wxOpenConfig; @Resource private UserService userService; @Override public LoginUserVO login(String data) { // 1. 解析 JSONObject jsonObject = JSONUtil.parseObj(data); String code = jsonObject.get("code", String.class); if (StringUtils.isAnyBlank(code)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数未传递"); } WxOAuth2AccessToken accessToken; try { WxMpService wxService = wxOpenConfig.getWxMpService(); accessToken = wxService.getOAuth2Service().getAccessToken(code); WxOAuth2UserInfo userInfo = wxService.getOAuth2Service().getUserInfo(accessToken, code); String unionId = userInfo.getUnionId(); String mpOpenId = userInfo.getOpenid(); if (StringUtils.isAnyBlank(unionId, mpOpenId)) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误"); } // 2. 获取请求上下文 todo 如果后期使用redis缓存登录信息,可以将这里删除 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求上下文未找到"); } HttpServletRequest request = attributes.getRequest(); return userService.userLoginByMpOpen(userInfo, request); } catch (Exception e) { log.error("userLoginByWxOpen error", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误"); } } } ``` ### 第四步:修改系统登录接口 ```java /** * 系统认证接口 * 负责系统的认证和注册 * * @author Sakura */ @RestController @RequestMapping("auth") public class AuthController { // region 登录相关 /** * 用户登录 * * @param authRequest 认证请求数据 * @return {@link LoginUserVO} */ @PostMapping("/login") public BaseResponse<LoginUserVO> userLogin(@RequestBody AuthRequest authRequest) { if (authRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } LoginUserVO loginUserVO = AuthStrategy.login(authRequest); return ResultUtils.success(loginUserVO); } } ``` ### 测试 请求接口 ```http POST localhost:8101/api/auth { "authType": "password", "data": "{\"userAccount\":\"admin\",\"userPassword\":\"k01225.k\"}" } ``` 结果 ```json { "code": 0, "data": { "id": "1", "userName": "admin", "userAvatar": "", "userProfile": "", "userRole": "admin", "createTime": "2025-03-10T23:03:18.000+00:00", "updateTime": "2025-03-10T23:10:31.000+00:00" }, "message": "ok" } ``` 那么现在系统的登录图, 这样就可以对外只提供一个登录接口,只需要传递不同的authType 即可完成不同的认证方式 <img src="https://pic.code-nav.cn/post_picture/1738491648276455426/Yi1lrxy9Yhdc1v3D.webp" alt="画板" width="100%" />
编程导航后端万用模板-Oss服务整合
利用S3 存储协议兼容S3协议的云厂商 模板:编程导航:springboot万用模板->springboot-init-master 参考:RuoYiPlus [https://gitee.com/dromara/RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) **为什么要整合多个Oss云厂商?** 1. 有时使用万用模板,不一定要使用模板中的cos,可能有其他选择:Oss,Minio等 **达成效果:** 1. 无感替换Oss云厂商。只需要修改配置文件中的Oss配置,皆可完成切换Oss云厂商。 <h1 id="tVAn1">引入依赖</h1> 此部分依赖参考RuoYiPlus 依赖 ```xml <!-- AWS SDK for Java 2.x --> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <exclusions> <!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 --> <exclusion> <groupId>software.amazon.awssdk</groupId> <artifactId>netty-nio-client</artifactId> </exclusion> <!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 --> <exclusion> <groupId>software.amazon.awssdk</groupId> <artifactId>aws-crt-client</artifactId> </exclusion> <!-- 将基于 Apache 的 HTTP 客户端从类路径中移除 --> <exclusion> <groupId>software.amazon.awssdk</groupId> <artifactId>apache-client</artifactId> </exclusion> <!-- 将配置基于 URL 连接的 HTTP 客户端从类路径中移除 --> <exclusion> <groupId>software.amazon.awssdk</groupId> <artifactId>url-connection-client</artifactId> </exclusion> </exclusions> </dependency> <!-- 使用AWS基于 CRT 的 S3 客户端 --> <dependency> <groupId>software.amazon.awssdk.crt</groupId> <artifactId>aws-crt</artifactId> <version>${aws.crt.version}</version> </dependency> <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 --> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3-transfer-manager</artifactId> <version>${aws.sdk.version}</version> </dependency> ``` 依赖版本 ```xml <!-- OSS 配置 --> <aws.sdk.version>2.28.22</aws.sdk.version> <aws.crt.version>0.31.3</aws.crt.version> ``` <h1 id="MkL6n">配置OSS客户端</h1> <h2 id="QZYbB">配置文件</h2> 在yml的文件中配置 ```yaml oss: # 对应云厂商的站点域名 endpoint: 127.0.0.1:9000 # 对应云厂商桶的绑定域名 domain: '' accessKey: UBCX82S9TLNq4WxkTBOJ secretKey: j0uPaCTvUKaOCU82tjFW8qdWeZxdsGOOp7mFX0yP # 前缀 prefix: '' # 存储空间名/桶名 bucketName: default # 存储区域 region: '' # 是否https(Y=是,N=否) isHttps: N # 桶权限类型(0private 1public 2custom) accessPolicy: 1 ``` <h2 id="gJ8l1">属性类</h2> ```java /** * OSS对象存储 配置属性 * @author sakura */ @Data @Component @ConfigurationProperties(prefix = "oss") public class OssProperties { /** * 访问站点 */ private String endpoint; /** * 自定义域名 */ private String domain; /** * ACCESS_KEY */ private String accessKey; /** * SECRET_KEY */ private String secretKey; /** * 存储空间名 */ private String bucketName; /** * 存储区域 */ private String region; /** * 是否https(Y=是,N=否) */ private String isHttps; /** * 桶权限类型(0private 1public 2custom) */ private String accessPolicy; ``` <h2 id="gJ8l1">OssClient</h2> ```java import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.IdUtil; import com.sakura.springbootinit.common.ErrorCode; import com.sakura.springbootinit.common.UploadResult; import com.sakura.springbootinit.constant.CommonConstant; import com.sakura.springbootinit.constant.OssConstant; import com.sakura.springbootinit.exception.OssException; import com.sakura.springbootinit.model.enums.AccessPolicyType; import com.sakura.springbootinit.model.enums.PolicyType; import com.sakura.springbootinit.model.properties.OssProperties; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.NoSuchBucketException; import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.model.*; import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.nio.file.Path; import java.time.Duration; import java.util.Date; /** * S3 存储协议 所有兼容S3协议的云厂商均支持 * 阿里云 腾讯云 七牛云 minio * * @author RuoYi-Plus */ public class OssClient { /** * 配置属性 */ private final OssProperties properties; /** * Amazon S3 异步客户端 */ private final S3AsyncClient client; /** * 用于管理 S3 数据传输的高级工具 */ private final S3TransferManager transferManager; /** * AWS S3 预签名 URL 的生成器 */ private final S3Presigner presigner; /** * 构造方法 * * @param ossProperties Oss配置属性 */ public OssClient(OssProperties ossProperties) { this.properties = ossProperties; try { // 创建 AWS 认证信息 StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); // MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问 boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE); // 创建AWS基于 CRT 的 S3 客户端 this.client = S3AsyncClient.crtBuilder() .credentialsProvider(credentialsProvider) .endpointOverride(URI.create(getEndpoint())) .region(of()) .targetThroughputInGbps(20.0) .minimumPartSizeInBytes(10 * 1025 * 1024L) .checksumValidationEnabled(false) .forcePathStyle(isStyle) .httpConfiguration(S3CrtHttpConfiguration.builder() .connectionTimeout(Duration.ofSeconds(60)) // 设置连接超时 .build()) .build(); //AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端 this.transferManager = S3TransferManager.builder().s3Client(this.client).build(); // 创建 S3 配置对象 S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false) .pathStyleAccessEnabled(isStyle).build(); // 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL this.presigner = S3Presigner.builder() .region(of()) .credentialsProvider(credentialsProvider) .endpointOverride(URI.create(getDomain())) .serviceConfiguration(config) .build(); // 创建存储桶 createBucket(); } catch (Exception e) { if (e instanceof OssException) { throw e; } throw new OssException(ErrorCode.SYSTEM_ERROR, "配置错误! 请检查系统配置:[" + e.getMessage() + "]"); } } /** * 同步创建存储桶 * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作 * * @throws OssException 当创建存储桶时发生异常时抛出 */ public void createBucket() { String bucketName = properties.getBucketName(); try { // 尝试获取存储桶的信息 client.headBucket( x -> x.bucket(bucketName) .build()) .join(); } catch (Exception ex) { if (ex.getCause() instanceof NoSuchBucketException) { try { // 存储桶不存在,尝试创建存储桶 client.createBucket( x -> x.bucket(bucketName)) .join(); // 设置存储桶的访问策略(Bucket Policy) client.putBucketPolicy( x -> x.bucket(bucketName) .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType()))) .join(); } catch (S3Exception e) { // 存储桶创建或策略设置失败 throw new OssException(ErrorCode.SYSTEM_ERROR, "创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); } } else { throw new OssException(ErrorCode.SYSTEM_ERROR, "判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]"); } } } /** * 上传文件到 Amazon S3,并返回上传结果 * * @param filePath 本地文件路径 * @param key 在 Amazon S3 中的对象键 * @param md5Digest 本地文件的 MD5 哈希值(可选) * @param contentType 文件内容类型 * @return UploadResult 包含上传后的文件信息 * @throws OssException 如果上传失败,抛出自定义异常 */ public UploadResult upload(Path filePath, String key, String md5Digest, String contentType) { try { // 构建上传请求对象 FileUpload fileUpload = transferManager.uploadFile( x -> x.putObjectRequest( y -> y.bucket(properties.getBucketName()) .key(key) .contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null) .contentType(contentType) // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) //.acl(getAccessPolicy().getObjectCannedACL()) .build()) .addTransferListener(LoggingTransferListener.create()) .source(filePath).build()); // 等待上传完成并获取上传结果 CompletedFileUpload uploadResult = fileUpload.completionFuture().join(); String eTag = uploadResult.response().eTag(); // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 return UploadResult.builder().url(getUrl() + CommonConstant.SLASH + key).filename(key).eTag(eTag).build(); } catch (Exception e) { // 捕获异常并抛出自定义异常 throw new OssException(ErrorCode.OPERATION_ERROR, "上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); } finally { // 无论上传是否成功,最终都会删除临时文件 FileUtil.del(filePath); } } /** * 上传 InputStream 到 Amazon S3 * * @param inputStream 要上传的输入流 * @param key 在 Amazon S3 中的对象键 * @param length 输入流的长度 * @param contentType 文件内容类型 * @return UploadResult 包含上传后的文件信息 * @throws OssException 如果上传失败,抛出自定义异常 */ public UploadResult upload(InputStream inputStream, String key, Long length, String contentType) { // 如果输入流不是 ByteArrayInputStream,则将其读取为字节数组再创建 ByteArrayInputStream if (!(inputStream instanceof ByteArrayInputStream)) { inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); } try { // 创建异步请求体(length如果为空会报错) BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder() .contentLength(length) .subscribeTimeout(Duration.ofSeconds(30)) .build(); // 使用 transferManager 进行上传 Upload upload = transferManager.upload( x -> x.requestBody(body) .putObjectRequest( y -> y.bucket(properties.getBucketName()) .key(key) .contentType(contentType) // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) //.acl(getAccessPolicy().getObjectCannedACL()) .build()) .build()); // 将输入流写入请求体 body.writeInputStream(inputStream); // 等待文件上传操作完成 CompletedUpload uploadResult = upload.completionFuture().join(); String eTag = uploadResult.response().eTag(); // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 return UploadResult.builder().url(getUrl() + CommonConstant.SLASH + key).filename(key).eTag(eTag).build(); } catch (Exception e) { throw new OssException(ErrorCode.OPERATION_ERROR, "上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); } } /** * 下载文件从 Amazon S3 到临时目录 * * @param path 文件在 Amazon S3 中的对象键 * @return 下载后的文件在本地的临时路径 * @throws OssException 如果下载失败,抛出自定义异常 */ public Path fileDownload(String path) { // 构建临时文件 Path tempFilePath = FileUtil.createTempFile().toPath(); // 使用 S3TransferManager 下载文件 FileDownload downloadFile = transferManager.downloadFile( x -> x.getObjectRequest( y -> y.bucket(properties.getBucketName()) .key(removeBaseUrl(path)) .build()) .addTransferListener(LoggingTransferListener.create()) .destination(tempFilePath) .build()); // 等待文件下载操作完成 downloadFile.completionFuture().join(); return tempFilePath; } /** * 下载文件从 Amazon S3 到 输出流 * * @param key 文件在 Amazon S3 中的对象键 * @param out 输出流 * @return 输出流中写入的字节数(长度) * @throws OssException 如果下载失败,抛出自定义异常 */ public long download(String key, OutputStream out) { try { // 构建下载请求 DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder() // 文件对象 .getObjectRequest(y -> y.bucket(properties.getBucketName()) .key(key) .build()) .addTransferListener(LoggingTransferListener.create()) // 使用订阅转换器 .responseTransformer(AsyncResponseTransformer.toBlockingInputStream()) .build(); // 使用 S3TransferManager 下载文件 Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest); // 输出到流中 try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream return responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread } } catch (Exception e) { throw new OssException(ErrorCode.OPERATION_ERROR, "文件下载失败,错误信息:[" + e.getMessage() + "]"); } } /** * 获取 S3 客户端的终端点 URL * * @return 终端点 URL */ public String getEndpoint() { // 根据配置文件中的是否使用 HTTPS,设置协议头部 String header = getIsHttps(); // 拼接协议头部和终端点,得到完整的终端点 URL return header + properties.getEndpoint(); } /** * 获取当前桶权限类型 * * @return 当前桶权限类型code */ public AccessPolicyType getAccessPolicy() { return AccessPolicyType.getByType(properties.getAccessPolicy()); } /** * 获取 S3 客户端的终端点 URL(自定义域名) * * @return 终端点 URL */ public String getDomain() { // 从配置中获取域名、终端点、是否使用 HTTPS 等信息 String domain = properties.getDomain(); String endpoint = properties.getEndpoint(); String header = getIsHttps(); // 如果是云服务商,直接返回域名或终端点 if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint; } // 如果是 MinIO,处理域名并返回 if (StringUtils.isNotEmpty(domain)) { return domain.startsWith(CommonConstant.HTTPS) || domain.startsWith(CommonConstant.HTTP) ? domain : header + domain; } // 返回终端点 return header + endpoint; } /** * 获取是否使用 HTTPS 的配置,并返回相应的协议头部。 * * @return 协议头部,根据是否使用 HTTPS 返回 "https://" 或 "http://" */ public String getIsHttps() { return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? CommonConstant.HTTPS : CommonConstant.HTTP; } /** * 获取云存储服务的URL * * @return 文件路径 */ public String getUrl() { String domain = properties.getDomain(); String endpoint = properties.getEndpoint(); String header = getIsHttps(); // 云服务商直接返回 if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint); } // MinIO 单独处理 if (StringUtils.isNotEmpty(domain)) { // 如果 domain 以 "https://" 或 "http://" 开头 return (domain.startsWith(CommonConstant.HTTPS) || domain.startsWith(CommonConstant.HTTP)) ? domain + CommonConstant.SLASH + properties.getBucketName() : header + domain + CommonConstant.SLASH + properties.getBucketName(); } return header + endpoint + CommonConstant.SLASH + properties.getBucketName(); } /** * 移除路径中的基础URL部分,得到相对路径 * * @param path 完整的路径,包括基础URL和相对路径 * @return 去除基础URL后的相对路径 */ public String removeBaseUrl(String path) { return path.replace(getUrl() + CommonConstant.SLASH, ""); } /** * 根据传入的 region 参数返回相应的 AWS 区域 * 如果 region 参数非空,使用 Region.of 方法创建并返回对应的 AWS 区域对象 * 如果 region 参数为空,返回一个默认的 AWS 区域(例如,us-east-1),作为广泛支持的区域 * * @return 对应的 AWS 区域对象,或者默认的广泛支持的区域(us-east-1) */ public Region of() { //AWS 区域字符串 String region = properties.getRegion(); // 如果 region 参数非空,使用 Region.of 方法创建对应的 AWS 区域对象,否则返回默认区域 return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1; } /** * 生成 AWS S3 存储桶访问策略 * * @param bucketName 存储桶 * @param policyType 桶策略类型 * @return 符合 AWS S3 存储桶访问策略格式的字符串 */ private static String getPolicy(String bucketName, PolicyType policyType) { String policy = switch (policyType) { case WRITE -> """ { "Version": "2012-10-17", "Statement": [] } """; case READ_WRITE -> """ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": [ "s3:GetBucketLocation", "s3:ListBucket", "s3:ListBucketMultipartUploads" ], "Resource": "arn:aws:s3:::bucketName" }, { "Effect": "Allow", "Principal": "*", "Action": [ "s3:AbortMultipartUpload", "s3:DeleteObject", "s3:GetObject", "s3:ListMultipartUploadParts", "s3:PutObject" ], "Resource": "arn:aws:s3:::bucketName/*" } ] } """; case READ -> """ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": ["s3:GetBucketLocation"], "Resource": "arn:aws:s3:::bucketName" }, { "Effect": "Deny", "Principal": "*", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::bucketName" }, { "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::bucketName/*" } ] } """; }; return policy.replaceAll("bucketName", bucketName); } /** * 上传 byte[] 数据到 Amazon S3,使用指定的后缀构造对象键。 * * @param data 要上传的 byte[] 数据 * @param suffix 对象键的后缀 * @return UploadResult 包含上传后的文件信息 * @throws OssException 如果上传失败,抛出自定义异常 */ public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) { return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length), contentType); } /** * 生成一个符合特定规则的、唯一的文件路径。通过使用日期、UUID、前缀和后缀等元素的组合,确保了文件路径的独一无二性 * * @param prefix 前缀 * @param suffix 后缀 * @return 文件路径 */ public String getPath(String prefix, String suffix) { // 生成uuid String uuid = IdUtil.fastSimpleUUID(); // 生成日期路径 Date now = new Date(); String datePath = DateFormatUtils.format(now, "yyyy/MM/dd"); // 拼接路径 String path = StringUtils.isNotEmpty(prefix) ? prefix + CommonConstant.SLASH + datePath + CommonConstant.SLASH + uuid : datePath + CommonConstant.SLASH + uuid; return path + suffix; } /** * 获取私有URL链接 * * @param objectKey 对象KEY * @param second 授权时间 */ public String getPrivateUrl(String objectKey, Integer second) { // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL URL url = presigner.presignGetObject( x -> x.signatureDuration(Duration.ofSeconds(second)) .getObjectRequest( y -> y.bucket(properties.getBucketName()) .key(objectKey) .build()) .build()) .url(); return url.toString(); } } ``` <h3 id="U8brK">用到的其他类</h3> ```java /** * 桶访问策略配置 * * @author RuoYi-Plus */ @Getter @AllArgsConstructor public enum AccessPolicyType { /** * private */ PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE), /** * public */ PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE), /** * custom */ CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ); /** * 桶 权限类型(数据库值) */ private final String type; /** * 桶 权限类型 */ private final BucketCannedACL bucketCannedACL; /** * 文件对象 权限类型 */ private final ObjectCannedACL objectCannedACL; /** * 桶策略类型 */ private final PolicyType policyType; public static AccessPolicyType getByType(String type) { for (AccessPolicyType value : values()) { if (value.getType().equals(type)) { return value; } } throw new RuntimeException("'type' not found By " + type); } } ``` ```java /** * minio策略配置 * * @author RuoYi-Plus */ @Getter @AllArgsConstructor public enum PolicyType { /** * 只读 */ READ("read-only"), /** * 只写 */ WRITE("write-only"), /** * 读写 */ READ_WRITE("read-write"); /** * 类型 */ private final String type; } ``` ```java /** * 处理Oss异常 */ public class OssException extends RuntimeException { /** * 错误码 */ private final int code; public OssException(int code, String message) { super(message); this.code = code; } public OssException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); } public OssException(ErrorCode errorCode, String message) { super(message); this.code = errorCode.getCode(); } public int getCode() { return code; } } ``` ```java /** * 上传返回体 * * @author RuoYi-Plus */ @Data @Builder public class UploadResult { /** * 文件路径 */ private String url; /** * 文件名 */ private String filename; /** * 已上传对象的实体标记(用来校验文件) */ private String eTag; } ``` ```java /** * 对象存储常量 * * @author RuoYi-Plus */ public interface OssConstant { /** * 云服务商 */ String[] CLOUD_SERVICE = new String[] {"aliyun", "qcloud", "qiniu", "obs"}; /** * https 状态 */ String IS_HTTPS = "Y"; } ``` ```java /** * 通用常量 * * @author <a href="https://github.com/liyupi">程序员鱼皮</a> * @from <a href="https://yupi.icu">编程导航知识星球</a> */ public interface CommonConstant { // .... /** * http请求 */ String HTTP = "http://"; /** * https请求 */ String HTTPS = "https://"; String SLASH = "/"; } ``` <h2 id="kCKxT">配置OssClient</h2> ```java /** * OSS 配置 * * @author sakura */ @Configuration public class OssConfig { /** * OSS 客户端 * @param ossProperties OSS 客户端 * @return OSS 客户端 */ @Bean public OssClient ossClient(OssProperties ossProperties) { return new OssClient(ossProperties); } } ``` <h1 id="kgpqx">实现Oss服务类</h1> 这里就不作数据库的存储管理了,直接返回可分为的url就好 <h2 id="pTjFa">上传方法</h2> ```java /** * OSS 服务实现类 */ @Slf4j @Service public class OssServiceImpl implements OssService { @Resource private OssClient storage; /** * 上传 MultipartFile 到对象存储服务 * * @param file 要上传的 MultipartFile 对象 * @return 上传成功后的返回可访问的URL信息 */ @Override public String upload(MultipartFile file) { String originalfileName = file.getOriginalFilename(); String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length()); UploadResult uploadResult; try { uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType()); } catch (IOException e) { throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage()); } return this.matchingUrl(uploadResult).getUrl(); } /** * 桶类型为 private 的URL 修改为临时URL时长为120s * * @return oss 匹配Url的OSS对象 */ private UploadResult matchingUrl(UploadResult oss) { // 仅修改桶类型为 private 的URL,临时URL时长为120s if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) { oss.setUrl(storage.getPrivateUrl(oss.getFilename(), 120)); } return oss; } } ``` <h1 id="X9i5D">编写接口</h1> 这里选择修改模板中的upload接口 ```java /** * 文件上传 * * @param multipartFile * @return */ @PostMapping("/upload") public BaseResponse<String> uploadFile(@RequestPart("file") MultipartFile multipartFile) { validFile(multipartFile); String url = ossService.upload(multipartFile); return ResultUtils.success(url); } ``` <h1 id="mS79F">测试</h1> 这里以Minio为例,不过多介绍minio安装和配置 <h2 id="xoVee">编写配置文件</h2> <img src="https://pic.code-nav.cn/post_picture/1738491648276455426/3RrEjqvolGjRyDc2.webp" alt="" width="100%" /> <h2 id="wPYuM">模拟请求</h2> <img src="https://pic.code-nav.cn/post_picture/1738491648276455426/WaUfHxm6O1Nw27H5.webp" alt="" width="100%" /> <h2 id="FTKBg">响应结果</h2> ```json { "code": 0, "data": "http://127.0.0.1:9000/default/2025/01/25/fe1f99c7e88c4354ba7138450223f361.jpg", "message": "ok" } ```
万用模板session设置为redis后报错
`> ` ```js ``` <img src="https://pic.code-nav.cn/post_picture/1670223536016338946/DBc5IleIdxS44mom.webp" alt="image.png" width="100%" /> ```js ```配置后报错不知道什么原因,有人遇到过吗 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.session.SessionAutoConfiguration$ServletSessionRepositoryValidator': Invocation of init method failed; nested exception is org.springframework.boot.autoconfigure.session.SessionRepositoryUnavailableException: No session repository could be auto-configured, check your configuration (session store type is 'redis')
后端万用模板是在哪个项目中搭建的呀 想学一下搭建的思路 然后自己搭建一个属于自己的后端模板
