parent
b5bb4909e4
commit
641a2e8d96
|
@ -1,28 +0,0 @@
|
|||
package com.guwan.backend.config;
|
||||
|
||||
import org.apache.hadoop.fs.FileSystem;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class HadoopConfig {
|
||||
|
||||
@Value("${hadoop.fs.defaultFS}")
|
||||
private String fsDefaultFS;
|
||||
|
||||
@Value("${hadoop.username}")
|
||||
private String username;
|
||||
|
||||
@Bean
|
||||
public FileSystem fileSystem() throws Exception {
|
||||
org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration();
|
||||
conf.set("fs.defaultFS", fsDefaultFS);
|
||||
// 设置副本数
|
||||
conf.set("dfs.replication", "3");
|
||||
// 设置块大小,适合大文件存储
|
||||
conf.set("dfs.blocksize", "128m");
|
||||
|
||||
return FileSystem.get(conf);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ public class SecurityConstants {
|
|||
public static final List<String> STATIC_RESOURCES = List.of(
|
||||
"/static/**", // 静态资源目录
|
||||
"/public/**", // 公共资源目录
|
||||
"/error" // 错误页面
|
||||
"/error", // 错误页面
|
||||
"/swagger-ui.html"
|
||||
);
|
||||
}
|
|
@ -1,22 +1,17 @@
|
|||
package com.guwan.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.guwan.backend.annotation.OperationLog;
|
||||
import com.guwan.backend.common.Result;
|
||||
import com.guwan.backend.dto.video.VideoDTO;
|
||||
import com.guwan.backend.service.VideoService;
|
||||
import com.guwan.backend.service.VideoRecommendService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "视频管理", description = "视频相关接口")
|
||||
|
@ -26,7 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
public class VideoController {
|
||||
|
||||
private final VideoService videoService;
|
||||
private final VideoRecommendService recommendService;
|
||||
|
||||
|
||||
@Operation(summary = "上传视频", description = "上传视频文件并返回视频信息")
|
||||
@SecurityRequirement(name = "bearer-jwt")
|
||||
|
@ -131,30 +126,7 @@ public class VideoController {
|
|||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取推荐视频")
|
||||
@SecurityRequirement(name = "bearer-jwt")
|
||||
@GetMapping("/recommend")
|
||||
public Result<List<VideoDTO>> getRecommendVideos(
|
||||
@Parameter(description = "返回数量") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
try {
|
||||
Long userId = securityUtil.getCurrentUserId();
|
||||
return Result.success(recommendService.getRecommendVideos(userId, limit));
|
||||
} catch (Exception e) {
|
||||
log.error("获取推荐视频失败", e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取相似视频")
|
||||
@GetMapping("/{id}/similar")
|
||||
public Result<List<VideoDTO>> getSimilarVideos(
|
||||
@Parameter(description = "视频ID") @PathVariable Long id,
|
||||
@Parameter(description = "返回数量") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
try {
|
||||
return Result.success(recommendService.getSimilarVideos(id, limit));
|
||||
} catch (Exception e) {
|
||||
log.error("获取相似视频失败", e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package com.guwan.backend.controller;
|
||||
|
||||
import com.guwan.backend.entity.Video;
|
||||
import com.guwan.backend.service.HdfsStorageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.ResourceRegion;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/stream")
|
||||
@RequiredArgsConstructor
|
||||
public class VideoStreamController {
|
||||
|
||||
private final HdfsStorageService hdfsStorageService;
|
||||
private static final long CHUNK_SIZE = 1024 * 1024; // 1MB chunks
|
||||
|
||||
@GetMapping("/video/{id}")
|
||||
public ResponseEntity<ResourceRegion> streamVideo(
|
||||
@PathVariable Long id,
|
||||
@RequestHeader(value = "Range", required = false) String rangeHeader) {
|
||||
try {
|
||||
// 获取视频路径
|
||||
Video video = videoMapper.selectById(id);
|
||||
if (video == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 获取视频流
|
||||
InputStream videoStream = hdfsStorageService.getVideoStream(video.getUrl());
|
||||
InputStreamResource resource = new InputStreamResource(videoStream);
|
||||
|
||||
// 获取视频元数据
|
||||
VideoMetadata metadata = hdfsStorageService.getVideoMetadata(video.getUrl());
|
||||
long contentLength = metadata.getSize();
|
||||
|
||||
// 处理Range请求
|
||||
HttpRange range = rangeHeader == null ? null : HttpRange.parseRanges(rangeHeader).get(0);
|
||||
ResourceRegion region;
|
||||
|
||||
if (range != null) {
|
||||
long start = range.getRangeStart(contentLength);
|
||||
long end = range.getRangeEnd(contentLength);
|
||||
long rangeLength = Math.min(CHUNK_SIZE, end - start + 1);
|
||||
region = new ResourceRegion(resource, start, rangeLength);
|
||||
} else {
|
||||
long rangeLength = Math.min(CHUNK_SIZE, contentLength);
|
||||
region = new ResourceRegion(resource, 0, rangeLength);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(region);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("视频流处理失败", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package com.guwan.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sms_log")
|
||||
public class SmsLog {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String type;
|
||||
|
||||
private String content;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@TableField("error_msg")
|
||||
private String errorMsg;
|
||||
|
||||
@TableField("created_time")
|
||||
private LocalDateTime createdTime;
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeepRecommendService {
|
||||
|
||||
private static final int EMBEDDING_SIZE = 128;
|
||||
private static final String MODEL_PATH = "models/recommend_model.pt";
|
||||
|
||||
private final NDManager manager;
|
||||
private final Model model;
|
||||
|
||||
public DeepRecommendService() {
|
||||
// 初始化DJL环境
|
||||
this.manager = NDManager.newBaseManager();
|
||||
this.model = Model.newInstance("recommend");
|
||||
loadModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频的嵌入向量
|
||||
*/
|
||||
public float[] getVideoEmbedding(Video video) {
|
||||
try {
|
||||
// 准备输入特征
|
||||
NDArray features = prepareVideoFeatures(video);
|
||||
|
||||
// 获取模型输出
|
||||
NDArray embedding = model.forward(new NDList(features)).singletonOrThrow();
|
||||
|
||||
// 转换为Java数组
|
||||
return embedding.toFloatArray();
|
||||
} catch (Exception e) {
|
||||
log.error("获取视频嵌入向量失败", e);
|
||||
return new float[EMBEDDING_SIZE];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的嵌入向量
|
||||
*/
|
||||
public float[] getUserEmbedding(List<UserBehavior> behaviors) {
|
||||
try {
|
||||
// 准备输入特征
|
||||
NDArray features = prepareUserFeatures(behaviors);
|
||||
|
||||
// 获取模型输出
|
||||
NDArray embedding = model.forward(new NDList(features)).singletonOrThrow();
|
||||
|
||||
// 转换为Java数组
|
||||
return embedding.toFloatArray();
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户嵌入向量失败", e);
|
||||
return new float[EMBEDDING_SIZE];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算相似度分数
|
||||
*/
|
||||
public double calculateSimilarityScore(float[] embedding1, float[] embedding2) {
|
||||
double dotProduct = 0.0;
|
||||
double norm1 = 0.0;
|
||||
double norm2 = 0.0;
|
||||
|
||||
for (int i = 0; i < embedding1.length; i++) {
|
||||
dotProduct += embedding1[i] * embedding2[i];
|
||||
norm1 += embedding1[i] * embedding1[i];
|
||||
norm2 += embedding2[i] * embedding2[i];
|
||||
}
|
||||
|
||||
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
|
||||
}
|
||||
|
||||
private void loadModel() {
|
||||
try {
|
||||
// 加载预训练模型
|
||||
Path modelPath = Paths.get(MODEL_PATH);
|
||||
model.load(modelPath);
|
||||
} catch (Exception e) {
|
||||
log.error("加载推荐模型失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private NDArray prepareVideoFeatures(Video video) {
|
||||
// 将视频特征转换为模型输入格式
|
||||
float[] features = new float[]{
|
||||
video.getDuration(),
|
||||
video.getViewCount(),
|
||||
video.getLikeCount(),
|
||||
// ... 其他特征
|
||||
};
|
||||
return manager.create(features);
|
||||
}
|
||||
|
||||
private NDArray prepareUserFeatures(List<UserBehavior> behaviors) {
|
||||
// 将用户行为特征转换为模型输入格式
|
||||
float[] features = new float[]{
|
||||
// 统计特征
|
||||
behaviors.size(),
|
||||
calculateAverageWatchDuration(behaviors),
|
||||
calculateLikeRatio(behaviors),
|
||||
// ... 其他特征
|
||||
};
|
||||
return manager.create(features);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
public interface FileService {
|
||||
String uploadBase64Image(String base64Image, String folder);
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hadoop.fs.FSDataInputStream;
|
||||
import org.apache.hadoop.fs.FSDataOutputStream;
|
||||
import org.apache.hadoop.fs.FileSystem;
|
||||
import org.apache.hadoop.fs.Path;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HdfsStorageService {
|
||||
|
||||
private final FileSystem fileSystem;
|
||||
private static final String VIDEO_PATH = "/videos";
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
/**
|
||||
* 上传视频到HDFS
|
||||
*/
|
||||
public String uploadVideo(MultipartFile file, String fileName) throws IOException {
|
||||
String hdfsPath = VIDEO_PATH + "/" + fileName;
|
||||
Path path = new Path(hdfsPath);
|
||||
|
||||
try (FSDataOutputStream out = fileSystem.create(path);
|
||||
BufferedInputStream in = new BufferedInputStream(file.getInputStream())) {
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int length;
|
||||
while ((length = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
return hdfsPath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HDFS读取视频流
|
||||
*/
|
||||
public InputStream getVideoStream(String path) throws IOException {
|
||||
Path hdfsPath = new Path(path);
|
||||
if (!fileSystem.exists(hdfsPath)) {
|
||||
throw new IOException("Video not found: " + path);
|
||||
}
|
||||
return fileSystem.open(hdfsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除HDFS中的视频
|
||||
*/
|
||||
public void deleteVideo(String path) throws IOException {
|
||||
Path hdfsPath = new Path(path);
|
||||
if (fileSystem.exists(hdfsPath)) {
|
||||
fileSystem.delete(hdfsPath, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频元数据
|
||||
*/
|
||||
public VideoMetadata getVideoMetadata(String path) throws IOException {
|
||||
Path hdfsPath = new Path(path);
|
||||
if (!fileSystem.exists(hdfsPath)) {
|
||||
throw new IOException("Video not found: " + path);
|
||||
}
|
||||
|
||||
return VideoMetadata.builder()
|
||||
.size(fileSystem.getFileStatus(hdfsPath).getLen())
|
||||
.blockSize(fileSystem.getFileStatus(hdfsPath).getBlockSize())
|
||||
.replication(fileSystem.getFileStatus(hdfsPath).getReplication())
|
||||
.modificationTime(fileSystem.getFileStatus(hdfsPath).getModificationTime())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
public interface VerificationService {
|
||||
void sendEmailCode(String email);
|
||||
void sendSmsCode(String phone);
|
||||
boolean verifyEmailCode(String email, String code);
|
||||
boolean verifySmsCode(String phone, String code);
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
import com.guwan.backend.dto.video.VideoDTO;
|
||||
import java.util.List;
|
||||
|
||||
public interface VideoRecommendService {
|
||||
// 获取推荐视频列表
|
||||
List<VideoDTO> getRecommendVideos(Long userId, int limit);
|
||||
|
||||
// 记录用户行为
|
||||
void recordUserBehavior(Long userId, Long videoId, String behaviorType, Integer watchDuration, Float watchProgress);
|
||||
|
||||
// 更新用户兴趣
|
||||
void updateUserInterests(Long userId);
|
||||
|
||||
// 计算视频相似度
|
||||
double calculateVideoSimilarity(Long videoId1, Long videoId2);
|
||||
|
||||
// 获取相似视频
|
||||
List<VideoDTO> getSimilarVideos(Long videoId, int limit);
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package com.guwan.backend.service.impl;
|
||||
|
||||
import com.guwan.backend.service.FileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Primary
|
||||
public class DummyFileServiceImpl implements FileService {
|
||||
|
||||
@Override
|
||||
public String uploadBase64Image(String base64Image, String folder) {
|
||||
log.info("模拟上传图片到文件夹: {}", folder);
|
||||
return "dummy/image/path.jpg";
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package com.guwan.backend.service.impl;
|
||||
|
||||
import com.guwan.backend.service.FileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
//@Service // 注释掉这个注解
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
@Value("${file.upload.path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Override
|
||||
public String uploadBase64Image(String base64Image, String folder) {
|
||||
try {
|
||||
// 解码Base64图片
|
||||
String[] parts = base64Image.split(",");
|
||||
byte[] imageBytes = Base64.getDecoder().decode(parts[1]);
|
||||
|
||||
// 生成文件名
|
||||
String fileName = UUID.randomUUID().toString() + ".jpg";
|
||||
String folderPath = uploadPath + "/" + folder;
|
||||
String filePath = folderPath + "/" + fileName;
|
||||
|
||||
// 创建目录
|
||||
new File(folderPath).mkdirs();
|
||||
|
||||
// 写入文件
|
||||
try (FileOutputStream fos = new FileOutputStream(filePath)) {
|
||||
fos.write(imageBytes);
|
||||
}
|
||||
|
||||
return folder + "/" + fileName;
|
||||
} catch (Exception e) {
|
||||
log.error("上传图片失败", e);
|
||||
throw new RuntimeException("上传图片失败", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
package com.guwan.backend.service.impl;
|
||||
|
||||
import com.guwan.backend.service.VideoRecommendService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VideoRecommendServiceImpl implements VideoRecommendService {
|
||||
|
||||
private final UserBehaviorMapper behaviorMapper;
|
||||
private final VideoTagWeightMapper tagWeightMapper;
|
||||
private final UserInterestMapper interestMapper;
|
||||
private final VideoMapper videoMapper;
|
||||
private final DeepRecommendService deepRecommendService;
|
||||
|
||||
@Override
|
||||
public List<VideoDTO> getRecommendVideos(Long userId, int limit) {
|
||||
// 1. 获取用户行为数据
|
||||
List<UserBehavior> behaviors = getRecentBehaviors(userId);
|
||||
|
||||
// 2. 获取用户嵌入向量
|
||||
float[] userEmbedding = deepRecommendService.getUserEmbedding(behaviors);
|
||||
|
||||
// 3. 获取候选视频
|
||||
List<Video> candidates = getCandidateVideos(userId);
|
||||
|
||||
// 4. 计算每个视频的推荐分数
|
||||
Map<Long, Double> scores = new HashMap<>();
|
||||
for (Video video : candidates) {
|
||||
// 获取视频嵌入向量
|
||||
float[] videoEmbedding = deepRecommendService.getVideoEmbedding(video);
|
||||
|
||||
// 计算相似度分数
|
||||
double similarityScore = deepRecommendService.calculateSimilarityScore(
|
||||
userEmbedding, videoEmbedding);
|
||||
|
||||
// 结合其他特征计算最终分数
|
||||
double finalScore = calculateFinalScore(similarityScore, video);
|
||||
scores.put(video.getId(), finalScore);
|
||||
}
|
||||
|
||||
// 5. 排序并返回推荐结果
|
||||
return candidates.stream()
|
||||
.sorted((v1, v2) -> Double.compare(scores.get(v2.getId()), scores.get(v1.getId())))
|
||||
.limit(limit)
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void recordUserBehavior(Long userId, Long videoId, String behaviorType,
|
||||
Integer watchDuration, Float watchProgress) {
|
||||
UserBehavior behavior = new UserBehavior();
|
||||
behavior.setUserId(userId);
|
||||
behavior.setVideoId(videoId);
|
||||
behavior.setBehaviorType(behaviorType);
|
||||
behavior.setWatchDuration(watchDuration);
|
||||
behavior.setWatchProgress(watchProgress);
|
||||
|
||||
behaviorMapper.insert(behavior);
|
||||
|
||||
// 异步更新用户兴趣
|
||||
updateUserInterests(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateUserInterests(Long userId) {
|
||||
// 1. 获取用户近期行为
|
||||
List<UserBehavior> behaviors = getRecentBehaviors(userId);
|
||||
|
||||
// 2. 计算每个标签的权重
|
||||
Map<String, Double> tagWeights = calculateTagWeights(behaviors);
|
||||
|
||||
// 3. 更新用户兴趣表
|
||||
updateUserInterestTable(userId, tagWeights);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double calculateVideoSimilarity(Long videoId1, Long videoId2) {
|
||||
// 基于标签的余弦相似度计算
|
||||
List<VideoTagWeight> tags1 = tagWeightMapper.selectByVideoId(videoId1);
|
||||
List<VideoTagWeight> tags2 = tagWeightMapper.selectByVideoId(videoId2);
|
||||
|
||||
return calculateCosineSimilarity(tags1, tags2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoDTO> getSimilarVideos(Long videoId, int limit) {
|
||||
// 1. 获取视频标签
|
||||
List<VideoTagWeight> sourceTags = tagWeightMapper.selectByVideoId(videoId);
|
||||
|
||||
// 2. 获取候选视频
|
||||
List<Video> candidates = getCandidateVideos(null);
|
||||
|
||||
// 3. 计算相似度并排序
|
||||
Map<Long, Double> similarities = new HashMap<>();
|
||||
for (Video candidate : candidates) {
|
||||
if (!candidate.getId().equals(videoId)) {
|
||||
double similarity = calculateVideoSimilarity(videoId, candidate.getId());
|
||||
similarities.put(candidate.getId(), similarity);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.stream()
|
||||
.filter(v -> !v.getId().equals(videoId))
|
||||
.sorted((v1, v2) -> Double.compare(similarities.get(v2.getId()), similarities.get(v1.getId())))
|
||||
.limit(limit)
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private double calculateFinalScore(double similarityScore, Video video) {
|
||||
// 结合多个因素计算最终分数
|
||||
double timeDecay = calculateTimeDecay(video.getCreatedTime());
|
||||
double popularityScore = calculatePopularityScore(video);
|
||||
|
||||
return similarityScore * 0.6 + // 相似度权重
|
||||
timeDecay * 0.2 + // 时间衰减权重
|
||||
popularityScore * 0.2; // 热度权重
|
||||
}
|
||||
|
||||
private double calculateTimeDecay(LocalDateTime createdTime) {
|
||||
long days = ChronoUnit.DAYS.between(createdTime, LocalDateTime.now());
|
||||
return 1.0 / (1.0 + Math.log1p(days));
|
||||
}
|
||||
|
||||
private double calculatePopularityScore(Video video) {
|
||||
return Math.log1p(video.getViewCount()) + Math.log1p(video.getLikeCount() * 2);
|
||||
}
|
||||
|
||||
private double calculateCosineSimilarity(List<VideoTagWeight> tags1, List<VideoTagWeight> tags2) {
|
||||
// 实现余弦相似度计算
|
||||
return 0.0;
|
||||
}
|
||||
}
|
|
@ -3,11 +3,12 @@ package com.guwan.backend.service.impl;
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.guwan.backend.annotation.OperationLog;
|
||||
import com.guwan.backend.dto.video.VideoDTO;
|
||||
import com.guwan.backend.entity.Video;
|
||||
import com.guwan.backend.entity.VideoLike;
|
||||
import com.guwan.backend.mapper.VideoMapper;
|
||||
import com.guwan.backend.mapper.VideoLikeMapper;
|
||||
import com.guwan.backend.mapper.VideoMapper;
|
||||
import com.guwan.backend.service.VideoService;
|
||||
import com.guwan.backend.util.MinioUtil;
|
||||
import com.guwan.backend.util.SecurityUtil;
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.minio.*;
|
|||
import io.minio.http.Method;
|
||||
import io.minio.messages.DeleteError;
|
||||
import io.minio.messages.DeleteObject;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
|
@ -17,7 +18,7 @@ import java.util.List;
|
|||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
|
|
|
@ -4,7 +4,12 @@ server:
|
|||
spring:
|
||||
application:
|
||||
name: backend
|
||||
|
||||
|
||||
# 视频上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
# 数据库配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
@ -66,16 +71,16 @@ spring:
|
|||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: javax.crypto.spec.SecretKeySpec@5884561.Guwan.javax.crypto.spec.SecretKeySpec@5884561
|
||||
expiration: 86400000 # 24小时
|
||||
jwt:
|
||||
secret: javax.crypto.spec.SecretKeySpec@5884561.Guwan.javax.crypto.spec.SecretKeySpec@5884561
|
||||
expiration: 86400000 # 24小时
|
||||
|
||||
# 阿里云配置
|
||||
aliyun:
|
||||
sms:
|
||||
aliyun:
|
||||
sms:
|
||||
|
||||
# MinIO配置
|
||||
minio:
|
||||
minio:
|
||||
endpoint: http://localhost:9000
|
||||
accessKey: admin
|
||||
secretKey: admin123456
|
||||
|
@ -84,20 +89,13 @@ spring:
|
|||
images: images # 图片桶
|
||||
|
||||
# 文件上传配置
|
||||
file:
|
||||
file:
|
||||
upload:
|
||||
path: D:/upload # Windows路径示例,根据实际情况修改
|
||||
path: D:/upload # Windows路径示例,根据实际情况修改
|
||||
|
||||
|
||||
|
||||
# 视频上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
|
||||
hadoop:
|
||||
fs:
|
||||
defaultFS: hdfs://localhost:9000
|
||||
username: hadoop
|
||||
|
||||
# Swagger配置
|
||||
springdoc:
|
||||
|
|
|
@ -18,20 +18,6 @@ CREATE TABLE IF NOT EXISTS `user` (
|
|||
UNIQUE KEY `uk_phone` (`phone`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
|
||||
|
||||
-- 短信日志表
|
||||
CREATE TABLE IF NOT EXISTS `sms_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID',
|
||||
`phone` varchar(20) NOT NULL COMMENT '手机号',
|
||||
`type` varchar(20) NOT NULL COMMENT '短信类型',
|
||||
`content` varchar(500) NOT NULL COMMENT '短信内容',
|
||||
`status` tinyint NOT NULL COMMENT '发送状态:0-失败,1-成功',
|
||||
`error_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
|
||||
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_phone` (`phone`),
|
||||
KEY `idx_created_time` (`created_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='短信发送日志表';
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sys_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
|
@ -50,4 +36,27 @@ CREATE TABLE IF NOT EXISTS `sys_log` (
|
|||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统操作日志';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统操作日志';
|
||||
|
||||
|
||||
|
||||
CREATE TABLE `video` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '视频ID',
|
||||
`title` varchar(100) NOT NULL COMMENT '视频标题',
|
||||
`description` text COMMENT '视频描述',
|
||||
`url` varchar(255) NOT NULL COMMENT '视频URL',
|
||||
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面图URL',
|
||||
`duration` bigint DEFAULT NULL COMMENT '视频时长(秒)',
|
||||
`size` bigint NOT NULL COMMENT '文件大小(字节)',
|
||||
`status` varchar(20) NOT NULL COMMENT '状态:DRAFT-草稿,PUBLISHED-已发布,DELETED-已删除',
|
||||
`user_id` bigint NOT NULL COMMENT '上传用户ID',
|
||||
`username` varchar(50) DEFAULT NULL COMMENT '上传用户名',
|
||||
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`view_count` int NOT NULL DEFAULT '0' COMMENT '观看次数',
|
||||
`like_count` int NOT NULL DEFAULT '0' COMMENT '点赞次数',
|
||||
`tags` varchar(255) DEFAULT NULL COMMENT '标签,逗号分隔',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_created_time` (`created_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='视频表';
|
Loading…
Reference in New Issue