项目介绍
本项目仿照bilbili,旨在提供一个前后端分离的视频平台,实现了视频的上传、点赞、收藏、弹幕、评论、个人主页内容与权限的编辑查看、私聊与大模型文生、智能ppt等功能。
Gateway服务
Controller
LoginController
采用了双token进行校验,当短token即将过期时,客户端可以使用长token来刷新短token,而不需要用户重新输入凭据。这通过以下步骤实现:
- 客户端发送一个请求,携带即将过期的短token和长token。
- 服务器验证长token的有效性。
- 如果长token有效,服务器生成一个新的短token并返回给客户端。
- 客户端更新短token,继续使用。
@PostMapping("/refreshToken")
public Result<Boolean> refreshToken(ServerWebExchange exchange){
log.info("刷新token");
String shortToken= exchange.getRequest().getHeaders().getFirst(SHORT_TOKEN);
log.info(shortToken);
String longToken=exchange.getRequest().getCookies().getFirst(LONG_TOKEN).getValue();
exchange.getResponse().getHeaders().set(SHORT_TOKEN, JwtUtil.refreshToken(shortToken,0));
ResponseCookie cookie = ResponseCookie.from(LONG_TOKEN, JwtUtil.refreshToken(longToken,1))
.httpOnly(true)
.path("/")
.maxAge(LONG_TOKEN_EXPIRATION )
.build();
exchange.getResponse().addCookie(cookie);
return Result.success(true);
}
Filter
这个过滤器的主要作用是:
- 记录请求路径,帮助跟踪和监控请求。
- 设置用户权限到Spring Security的上下文中,以便在后续的请求处理中进行授权检查。
Common服务
Client
searchClient
使用feign来进行服务间通信,使其他微服务来调用search服务的功能。
- 定义 Feign 客户端接口:创建一个接口,并使用 @FeignClient 注解来指定要调用的服务名称和相关配置。然后在接口的方法上使用注解(如 @GetMapping、@PostMapping 等)来定义具体的请求路径和方法。
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "search-service", url = "http://search-service:8201")
public interface SearchClient {
@GetMapping("/search/likelyVideoRecommend/{videoId}")
List<RecommendVideo> getRecommendVideo(@PathVariable String videoId);
}
videoClient
这个 VideoClient 接口定义了与视频服务通信的三个方法,分别用于上传视频、获取视频流和上传视频封面。通过 Feign 客户端,可以非常方便地调用这些远程服务,就像调用本地方法一样简单。
@Component
@FeignClient(name = "video", url = "http://localhost:10201")
public interface VideoClient {
@PostMapping(value = "/videoEncode/uploadVideo", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadVideo(@RequestPart("multipartFile") MultipartFile multipartFile);
@PostMapping("/videoEncode/getVideoInputStream")
ResponseEntity<Resource> getVideo(@RequestBody UploadVideo uploadVideo);
@PostMapping(value = "/videoEncode/uploadVideoCover", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadVideoCover(@RequestPart("multipartFile") MultipartFile multipartFile);
}
noticeClient
SendNoticeClient 是一个使用 Spring Cloud OpenFeign 的 Feign 客户端接口,用于与通知服务进行通信。它定义了多个方法,用于发送不同类型的通知,包括动态通知、数据库变更通知、点赞通知、评论通知、聊天通知和上传通知。
CustomMultipartFile
MultipartFile 是 Spring Framework 中的一个接口,主要用于处理文件上传。它提供了对上传文件的访问和操作功能,使得在 Spring MVC 应用程序中处理文件上传变得非常方便。
这个 CustomMultipartFile 类是一个自定义的 MultipartFile 实现,用于在远程调用时传递输入流(InputStream)。它的主要作用是将输入流包装成一个 MultipartFile 对象,以便在 Spring 的文件上传相关方法中使用。
UserCenter服务
编辑用户信息
处理逻辑,判断用户是否更改id、头像、简介,然后修改数据库并调用client发送同步消息,保证数据一致性。
/**
* 修改用户信息并发送数据同步消息
*/
@Override
public Result<Boolean> editSelfInfo(MultipartFile file, Integer userId, String nickname, String intro) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
// 初始化存储更新信息的 Map
Map<String, Object> map = new HashMap<>();
// MybatisPlus构建更新条件,确保只更新指定 userId 的用户信息
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, userId);
// 如果用户上传了头像文件
if (file != null) {
// 生成随机文件名,避免文件名冲突
String coverName = UUID.randomUUID().toString().substring(0, 10) + file.getOriginalFilename();
// 将文件上传到 MinIO 对象存储
minioClient.putObject(
PutObjectArgs.builder()
.contentType(file.getContentType())
.stream(file.getInputStream(), -1, 10485760)
.bucket(bucketName)
.object(coverName)
.build()
);
// 生成文件的访问 URL
String url = filePath + bucketName + "/" + coverName;
// 将头像 URL 存储到 Map 和更新条件中
map.put(USER_COVER, url);
wrapper.set(User::getCover, url);
}
// 如果用户提供了新的昵称
if (nickname != null) {
// 将昵称存储到 Map 和更新条件中
map.put(USER_NICKNAME, nickname);
wrapper.set(User::getNickname, nickname);
}
// 如果用户提供了新的简介
if (intro != null) {
// 将简介存储到 Map 和更新条件中
map.put(USER_INTRO, intro);
wrapper.set(User::getIntro, intro);
}
// 更新数据库中的用户信息
userMapper.update(null, wrapper);
// 构建通知消息的 Map
map.put(TABLE_ID, userId);
map.put(OPERATION_TYPE, OPERATION_TYPE_UPDATE);
map.put(TABLE_NAME, USER_TABLE_NAME);
// 发送数据同步通知,告知其他系统或服务用户信息已更新
sendNoticeClient.sendDBChangeNotice(map);
// 返回成功响应
return Result.success(true);
}
Video服务
视频上传
分片上传: 将大视频文件分割成多个分片,逐个上传到 MinIO。
断点续传: 通过记录已上传的分片信息,支持在网络波动时继续上传未完成的分片。
封面提取: 在上传过程中从视频中提取封面,转换为 Base64 编码。
分片合并: 当所有分片上传完成后,合并分片生成完整的视频文件。
/**
* 上传视频时获取视频封面
*/
@Override
public Result<List<String>> uploadPart(UploadPartRequest uploadPartRequest) throws IOException, EncoderException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
// 提取分片标识符的有效部分
int commaIndex = uploadPartRequest.getResumableIdentifier().indexOf(',');
uploadPartRequest.setResumableIdentifier(uploadPartRequest.getResumableIdentifier().substring(0, commaIndex));
String resumableIdentifier = uploadPartRequest.getResumableIdentifier();
// 初始化视频名称和封面
String videoName = "";
String videoCover = "";
// 检查是否需要提取视频封面
if (uploadPartMap.get(resumableIdentifier) == null || uploadPartMap.get(resumableIdentifier).getHasCutImg() == false) {
// 获取视频文件输入流
InputStream videoFileInputStream = uploadPartRequest.getFile().getInputStream();
byte[] bytes = IoUtil.readBytes(videoFileInputStream);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 创建临时文件路径和文件名
String filePath = Files.createTempDirectory(".tmp").toString();
String coverFileName = UUID.randomUUID().toString().substring(0, 10) + ".jpg";
String videoFileName = "video";
File directory = new File(filePath);
File videoFile = new File(filePath, videoFileName);
File coverFile = new File(directory, coverFileName);
// 将视频数据写入临时文件
Files.copy(byteArrayInputStream, Paths.get(videoFile.getAbsolutePa