微服务的编程测评系统15-头像上传-OSS
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. 头像上传
- 1.1 引入oss
- 1.2 集成oss到项⽬
- 1.3 文件上传功能
- 1.4 测试
- 1.5 头像上传
- 2. 用户答题功能
- 2.1 表结构设计
- 总结
前言
1. 头像上传
1.1 引入oss
要先创建AcessKey,头像那里点击AcessKey
搜索Oss对象存储
可以点击免费试用,过期了就买新的
点击试用教程
点击20GB3个月的,立即试用
点击新手秘籍
然后是创建Bucket
然后进入bucket
然后是点击新建目录,就可以创建我们要的目录了
1.2 集成oss到项⽬
找到下面的sdk下载
点击sdk示例,选择java
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.17.4</version>
</dependency>
先创建oj-common-file
如果使用的是Java 9及以上的版本,则需要添加以下JAXB相关依赖。
<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version>
</dependency>
<dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.3</version>
</dependency>
还有
<dependency><groupId>com.ck</groupId><artifactId>oj-common-core</artifactId><version>${oj-common-core.version}</version></dependency><dependency><groupId>com.ck</groupId><artifactId>oj-common-redis</artifactId><version>${oj-common-redis.version}</version></dependency><dependency><groupId>com.ck</groupId><artifactId>oj-common-security</artifactId><version>${oj-common-security.version}</version></dependency>
然后是配置访问凭证——》只有有这个凭证的人才可以使用,就按照官网上面的来操作就可以了–>可以后面用nacos配置
注意要在:在 RAM 控制台,创建使用永久 AccessKey 访问的 RAM 用户,保存 AccessKey,然后为该用户授予 AliyunOSSFullAccess 权限。–》生成AccessKey
然后是初始化了
这个就是初始化示例
endpoint就是我们创建bucket的地域
我们是成都
就是oss-cn-chengdu.aliyuncs.com
region就是cn-chengdu
这个最好也写在nacos上
credentialsProvider这里是配置访问凭证
我们用的不是环境变量,是nacos配置的访问凭证
// 创建 OSS 客户端实例OSS ossClient = OSSClientBuilder.create()
这个是最重要的
但是这个应该是交给spring管理的,我们就不用最后还关闭了
file下创建config包
OSSProperties
@Data
@Component
@ConfigurationProperties(prefix = "file.oss")
public class OSSProperties {private String endpoint;//地域private String region;//地域private String accessKeyId;private String accessKeySecret;private String bucketName;//bucket名字/*** 路径前缀,加在 endPoint 之后,就是目录*/private String pathPrefix; //ojtest,bucket的目录名
}
@ConfigurationProperties(prefix = “file.oss”)的意思就是把nacos上的配置读取到这个类
@Slf4j
@Configuration
@EnableConfigurationProperties(OSSProperties.class)//Component注解的bean不好找,只能这样了
public class OSSConfig {@Autowiredprivate OSSProperties prop;public OSS ossClient;@Beanpublic OSS ossClient() throws ClientException {DefaultCredentialProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(prop.getAccessKeyId(), prop.getAccessKeySecret());// 创建ClientBuilderConfigurationClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);// 使用内网endpoint进行上传ossClient = OSSClientBuilder.create().endpoint(prop.getEndpoint()).credentialsProvider(credentialsProvider).clientConfiguration(clientBuilderConfiguration).region(prop.getRegion()).build();return ossClient;}@PreDestroypublic void closeOSSClient() {ossClient.shutdown();}
}
这样就可以了
加上 @PreDestroy,那么在销毁这个对象的时候,就会自动执行这个方法了
nacos配置
file:max-time: 3test: trueoss:endpoint: oss-cn-chengdu.aliyuncs.comregion: cn-chengduaccessKeyId: 你的accessKeyIdaccessKeySecret: 你的accessKeySecretbucketName: 你的bucketNamepathPrefix: ojtest/
1.3 文件上传功能
我们使用的主要是这个
我们可以分装成一个service
来调用
@Getter
@Setter
public class OSSResult {private String name;/*** 对象状态:true成功,false失败*/private boolean success;
}
@Slf4j
@Service
@RefreshScope
public class OSSService {@Autowiredprivate OSSProperties prop;@Autowiredprivate OSSClient ossClient;@Autowiredprivate RedisService redisService;@Value("${file.max-time}")private int maxTime;@Value("${file.test}")private boolean test;public OSSResult uploadFile(MultipartFile file) throws Exception {if (!test) {checkUploadCount();}InputStream inputStream = null;try {String fileName;if (file.getOriginalFilename() != null) {fileName = file.getOriginalFilename().toLowerCase();} else {fileName = "a.png";}String extName = fileName.substring(fileName.lastIndexOf(".") + 1);inputStream = file.getInputStream();return upload(extName, inputStream);} catch (Exception e) {log.error("OSS upload file error", e);throw new ServiceException(ResultCode.FAILED_FILE_UPLOAD);} finally {if (inputStream != null) {inputStream.close();}}}private void checkUploadCount() {Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);Long times = redisService.getCacheMapValue(CacheConstants.USER_UPLOAD_TIMES_KEY, String.valueOf(userId), Long.class);if (times != null && times >= maxTime) {throw new ServiceException(ResultCode.FAILED_FILE_UPLOAD_TIME_LIMIT);}redisService.incrementHashValue(CacheConstants.USER_UPLOAD_TIMES_KEY, String.valueOf(userId), 1);if (times == null || times == 0) {long seconds = ChronoUnit.SECONDS.between(LocalDateTime.now(),LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0));redisService.expire(CacheConstants.USER_UPLOAD_TIMES_KEY, seconds, TimeUnit.SECONDS);}}private OSSResult upload(String fileType, InputStream inputStream) {// key pattern: file/id.xxx, cannot start with /String key = prop.getPathPrefix() + ObjectId.next() + "." + fileType;ObjectMetadata objectMetadata = new ObjectMetadata();objectMetadata.setObjectAcl(CannedAccessControlList.PublicRead);PutObjectRequest request = new PutObjectRequest(prop.getBucketName(), key, inputStream, objectMetadata);PutObjectResult putObjectResult;try {putObjectResult = ossClient.putObject(request);} catch (Exception e) {log.error("OSS put object error: {}", ExceptionUtil.stacktraceToOneLineString(e, 500));throw new ServiceException(ResultCode.FAILED_FILE_UPLOAD);}return assembleOSSResult(key, putObjectResult);}private OSSResult assembleOSSResult(String key, PutObjectResult putObjectResult) {OSSResult ossResult = new OSSResult();if (putObjectResult == null || StrUtil.isBlank(putObjectResult.getRequestId())) {ossResult.setSuccess(false);} else {ossResult.setSuccess(true);ossResult.setName(FileUtil.getName(key));}return ossResult;}
}
uploadFile就是上传文件
checkUploadCount是检查上传次数,文件上传次数是有限制的,防止一直上传,钱不够用了
public Long incrementHashValue(final String key, final String hKey, long delta) {return redisTemplate.opsForHash().increment(key, hKey, delta);}
public static final String USER_UPLOAD_TIMES_KEY = "user:upload:times";
我们这里用的是incrementHashValue来对上传次数进行计数,用的是hash的办法,hash一共有两个key,一个是内存keyUSER_UPLOAD_TIMES_KEY ,一个是外存key:userId
其实和以前String类型的计数是差不多的
delta表示增加的数是多少,就表示加多少
uploadFile中test是一个开关,表示要不要无限上传图片
MultipartFile是接收前端上传的文件类
它的getInputStream方法就可以获取到InputStream了
如果OSS文件存在,则上传的数据会覆盖该文件的内容;如果OSS文件不存在,则会新建该文件。
我们上传文件到oss,但是如果文件名重复了,那就不好了,以前人的头像就没了
所以我们第一要拿到原本文件名的后缀,然后给它新建一个文件名,保证不重复
upload就是上传文件方法了
upload中prop.getPathPrefix()就是oj-test的目录
ObjectId.next()就是生成一个不一样的文件名,也是hutool的,生成的是字符串
assembleOSSResult就是来处理一下发送是否成功,然后对应应该返回的OssResult应该是什么
1.4 测试
我们这里上传文件分为两个接口
一个是上传文件到oss
一个是把返回的唯一标识文件名存到数据库中
如果两个在一起操作,就会速度很慢,防止一个接口5s搞不赢就超时了
OSSResult中的name就是文件名就是唯一标识
@RestController
@RequestMapping("/file")
@Tag(name = "文件上传接口")
@Slf4j
public class FileController extends BaseController {@Autowiredprivate IFileService fileService;@PostMapping("/upload")public R<OSSResult> upload(@RequestBody MultipartFile file) {return R.ok(fileService.upload(file));}
}
@Slf4j
@Configuration
public class OSSConfig {
注意这里应该删掉@EnableConfigurationProperties(OSSProperties.class)
应该这个注解就是一个注册,而@component也是一个注册,这样就会双重注册,所以就不行
注意了这里测试的接口应该是form-data,然后是file类型
记得上传文件
在OSSService中
这里改一下
@Autowiredprivate OSS ossClient;
注意我们还要开启oss的读取权限
这样就可以了
测试发现也是存储成功了
1.5 头像上传
存储成功了,我们还要查询头像才行
而且图片文件名还要存在mysql,这样就可以一一对应了,和用户
而且我们说过的,上传oss和存数据库应该分开的,因为上传oss很耗时间
先写后端
@PutMapping("/head-image/update")public R<Void> updateHeadImage(@RequestBody UserUpdateDTO userUpdateDTO) {return toR(userService.updateHeadImage(userUpdateDTO));}
@Overridepublic int updateHeadImage(UserUpdateDTO userUpdateDTO) {Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);if (userId == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}User user = userMapper.selectById(userId);if (user == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}user.setHeadImage(userUpdateDTO.getHeadImage());//更新用户缓存userCacheManager.refreshUser(user);tokenService.refreshLoginUser(user.getNickName(),user.getHeadImage(),ThreadLocalUtil.get(Constants.USER_KEY, String.class));return userMapper.updateById(user);}
这样就成功了
前端上传头像我们用的是el-upload
就用第一个就可以了
<div v-if="!isDisabled()"><el-upload :action="uploadUrl" :headers="headers":on-error="handleUploadError" :on-success="handleUploadSuccess" :show-file-list="false"><el-button type="info">{{ userDetailForm.headImage ? '更换头像' : '上传头像' }}</el-button><template #tip> </template></el-upload></div>
所以action就是配置上传文件的接口,
//http://127.0.0.1:19090/friend/file/upload
const uploadUrl = ref("/dev-api/file/upload")
所以这个意思就是上传本地文件之后,自动调用/file/upload
注意这个action发送的请求不是axios发送的请求,所以我们以前弄的axios的请求拦截器配置的请求头就不会对这个upload生效了
所以我们要用:headers="headers"来配置请求头
const headers = ref({Authorization: "Bearer " + getToken(),
})
所以说呢,这个action的请求和响应都必须我们自己来处理,不能用原来的拦截器
上传头像之后,还要存入唯一标识
这两个就可以处理上传成功和失败的情况了
:on-error=“handleUploadError” :on-success=“handleUploadSuccess”
handleUploadSuccess方法的res就是响应结果,就是R,不用再次data才能得到R
async function handleUploadSuccess(res) {if (res.code !== 1000) {ElMessage.error(res.msg)} else {const userUpdateDTO = reactive({headImage : res.data.name})await updateHeadImageService(userUpdateDTO)getUserDetail()ElMessage.success("头像上传成功")window.location.reload();}
}
window.location.reload();是刷新页面的意思,这里刷新一下的原因是右上角的头像没有更新
但是存入了oss,怎么获取oss中的图片呢
怎么加载到前端呢—》一个下载资源的地址
下载的url就在这里,发现前缀都是固定的
所以我们在后端获取detail和info的时候就可以加上前缀url,这样就可以给前端加载了
if(StrUtil.isNotEmpty(loginUser.getHeadImage())){loginUserVO.setHeadImage(downLoadUrl+loginUser.getHeadImage());}
if(StrUtil.isNotEmpty(userVO.getHeadImage())){userVO.setHeadImage(downLoadUrl+userVO.getHeadImage());}
其中downLoadUrl用nacos配置
export function updateHeadImageService(params = {}) {return service({url: "/user/head-image/update",method: "put",data: params,});
}
这样就成功了
2. 用户答题功能
2.1 表结构设计
左边是标题和内容
上边是执行代码
答题和判题是不一样的功能
答题在题库和竞赛都有
竞赛答题那里,应该要对用户总分和排名进行判断(竞赛结束)----》所以要对答题的代码,分数进行存储
我们要对答题记录进行保存—》也要保存代码,分数–》存最近一次记录
用户提交表
create table tb_user_submit(
submit_id bigint unsigned NOT NULL COMMENT '提交记录id',
user_id bigint unsigned NOT NULL COMMENT '用户id',
question_id bigint unsigned NOT NULL COMMENT '题目id',
exam_id bigint unsigned COMMENT '竞赛id',
program_type tinyint NOT NULL COMMENT '代码类型 0 java 1 CPP',
user_code text NOT NULL COMMENT '用户代码',
pass tinyint NOT NULL COMMENT '0:未通过 1:通过',
exe_message varchar(500) NOT NULL COMMENT '执行结果',
score int NOT NULL DEFAULT '0' COMMENT '得分',
create_by bigint unsigned not null comment '创建人',
create_time datetime not null comment '创建时间',
update_by bigint unsigned comment '更新人',
update_time datetime comment '更新时间',
primary key(`submit_id`)
)
比赛的时候有examId,其他没有