feat(oss): 实现文件上传功能并集成Feign调用

- 新增文件上传接口,支持multipart/form-data格式
- 配置Spring Servlet multipart参数,设置文件大小限制
- 添加Feign客户端配置,支持表单提交
- 实现Feign请求拦截器,传递用户上下文信息
- 创建OSS服务API接口,用于文件上传
- 在用户服务中集成OSS RPC调用,实现头像和背景图上传
- 添加上传失败的业务异常处理
- 更新pom.xml依赖,引入OpenFeign、负载均衡及Feign表单相关组件
- 定义API常量和服务名称
- 启用Feign客户端扫描,支持跨服务调用
This commit is contained in:
Hanserwei
2025-10-04 15:53:22 +08:00
parent 91e36d5a84
commit 4b992c35ca
18 changed files with 215 additions and 5 deletions

View File

@@ -20,5 +20,24 @@
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Feign 表单提交 -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.oss.api;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.config.FeignFormConfig;
import com.hanserwei.hannote.oss.constant.ApiConstants;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
@FeignClient(name = ApiConstants.SERVICE_NAME, configuration = FeignFormConfig.class)
public interface FileFeignApi {
String PREFIX = "/file";
/**
* 文件上传
*
* @param file 文件
* @return 文件上传结果
*/
@PostMapping(value = PREFIX + "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Response<?> uploadFile(@RequestPart(value = "file") MultipartFile file);
}

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.oss.config;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignFormConfig {
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder();
}
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.oss.constant;
public interface ApiConstants {
/**
* 服务名称
*/
String SERVICE_NAME = "han-note-oss";
}

View File

@@ -75,6 +75,23 @@
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss-api</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -1,5 +1,6 @@
package com.hanserwei.hannote.oss.controller;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.service.FileService;
import jakarta.annotation.Resource;
@@ -21,6 +22,7 @@ public class FileController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Response<?> uploadFile(@RequestPart(value = "file") MultipartFile file) {
log.info("当前用户 ID: {}", LoginUserContextHolder.getUserId());
return fileService.uploadFile(file);
}

View File

@@ -4,6 +4,10 @@ server:
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境
servlet:
multipart:
max-file-size: 20MB # 单个文件最大大小
max-request-size: 100MB # 单次请求最大大小(包含多个文件)
storage:
type: rustfs # 对象存储类型

View File

@@ -66,6 +66,10 @@
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss-api</artifactId>
</dependency>
</dependencies>

View File

@@ -3,9 +3,11 @@ package com.hanserwei.hannote.user.biz;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@MapperScan("com.hanserwei.hannote.user.biz.domain.mapper")
@EnableFeignClients(basePackages = "com.hanserwei.hannote")
public class HannoteUserBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteUserBizApplication.class, args);

View File

@@ -17,7 +17,8 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
HAN_NOTE_ID_VALID_FAIL("USER-20002", "小憨书号请设置6-15个字符仅可使用英文必须、数字、下划线"),
SEX_VALID_FAIL("USER-20003", "性别错误"),
INTRODUCTION_VALID_FAIL("USER-20004", "个人简介请设置1-100个字符"),
;
UPLOAD_AVATAR_FAIL("USER-20005", "头像上传失败"),
UPLOAD_BACKGROUND_IMG_FAIL("USER-20006", "背景图上传失败"),
;
// 异常码

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.user.biz.rpc;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.api.FileFeignApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component
public class OssRpcService {
@Resource
private FileFeignApi fileFeignApi;
public String uploadFile(MultipartFile file) {
// 调用对象存储服务上传文件
Response<?> response = fileFeignApi.uploadFile(file);
if (!response.isSuccess()) {
return null;
}
// 返回图片访问链接
return (String) response.getData();
}
}

View File

@@ -3,6 +3,7 @@ package com.hanserwei.hannote.user.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.ParamUtils;
import com.hanserwei.hannote.user.biz.domain.dataobject.UserDO;
@@ -10,7 +11,9 @@ import com.hanserwei.hannote.user.biz.domain.mapper.UserDOMapper;
import com.hanserwei.hannote.user.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.user.biz.enums.SexEnum;
import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
import com.hanserwei.hannote.user.biz.rpc.OssRpcService;
import com.hanserwei.hannote.user.biz.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -23,6 +26,10 @@ import java.util.Objects;
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implements UserService {
@Resource
private OssRpcService ossRpcService;
@Override
public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
UserDO userDO = new UserDO();
@@ -35,7 +42,16 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
MultipartFile avatar = updateUserInfoReqVO.getAvatar();
if (Objects.nonNull(avatar)) {
// TODO: 上传头像,调用服务
String avatarUrl = ossRpcService.uploadFile(avatar);
log.info("==> 调用 oss 服务成功上传头像url{}", avatarUrl);
// 若上传头像失败,则抛出业务异常
if (StringUtils.isBlank(avatarUrl)) {
throw new ApiException(ResponseCodeEnum.UPLOAD_AVATAR_FAIL);
}
userDO.setAvatar(avatarUrl);
needUpdate = true;
}
// 昵称
@@ -80,7 +96,16 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
// 背景图片
MultipartFile backgroundImg = updateUserInfoReqVO.getBackgroundImg();
if (Objects.nonNull(backgroundImg)) {
// TODO: 上传背景图片,调用服务
String backgroundImgUrl = ossRpcService.uploadFile(backgroundImg);
log.info("==> 调用 oss 服务成功上传背景图url{}", backgroundImg);
// 若上传背景图失败,则抛出业务异常
if (StringUtils.isBlank(backgroundImgUrl)) {
throw new ApiException(ResponseCodeEnum.UPLOAD_BACKGROUND_IMG_FAIL);
}
userDO.setBackgroundImg(backgroundImgUrl);
needUpdate = true;
}
if (needUpdate) {

View File

@@ -4,6 +4,10 @@ server:
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境
servlet:
multipart:
max-file-size: 20MB # 单个文件最大大小
max-request-size: 100MB # 单次请求最大大小(包含多个文件)
mybatis-plus:
configuration:
map-underscore-to-camel-case: true

View File

@@ -43,6 +43,9 @@
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,14 @@
package com.hanserwei.framework.biz.context.config;
import com.hanserwei.framework.biz.context.interceptor.FeignRequestInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class FeignContextAutoConfiguration {
@Bean
public FeignRequestInterceptor feignRequestInterceptor() {
return new FeignRequestInterceptor();
}
}

View File

@@ -0,0 +1,25 @@
package com.hanserwei.framework.biz.context.interceptor;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.constant.GlobalConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取当前上下文中的用户 ID
Long userId = LoginUserContextHolder.getUserId();
// 若不为空,则添加到请求头中
if (Objects.nonNull(userId)) {
requestTemplate.header(GlobalConstants.USER_ID, String.valueOf(userId));
log.info("########## feign 请求设置请求头 userId: {}", userId);
}
}
}

View File

@@ -1 +1,2 @@
com.hanserwei.framework.biz.context.config.ContextAutoConfiguration
com.hanserwei.framework.biz.context.config.FeignContextAutoConfiguration

12
pom.xml
View File

@@ -50,6 +50,7 @@
<activation.version>1.1.1</activation.version>
<jaxb-runtime.version>2.3.3</jaxb-runtime.version>
<cos-api.version>5.6.227</cos-api.version>
<feign-form.version>3.8.0</feign-form.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -203,6 +204,17 @@
<artifactId>cos_api</artifactId>
<version>${cos-api.version}</version>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- Feign 表单提交 -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>${feign-form.version}</version>
</dependency>
</dependencies>
</dependencyManagement>