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(
|
public static final List<String> STATIC_RESOURCES = List.of(
|
||||||
"/static/**", // 静态资源目录
|
"/static/**", // 静态资源目录
|
||||||
"/public/**", // 公共资源目录
|
"/public/**", // 公共资源目录
|
||||||
"/error" // 错误页面
|
"/error", // 错误页面
|
||||||
|
"/swagger-ui.html"
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,22 +1,17 @@
|
||||||
package com.guwan.backend.controller;
|
package com.guwan.backend.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.guwan.backend.annotation.OperationLog;
|
|
||||||
import com.guwan.backend.common.Result;
|
import com.guwan.backend.common.Result;
|
||||||
import com.guwan.backend.dto.video.VideoDTO;
|
import com.guwan.backend.dto.video.VideoDTO;
|
||||||
import com.guwan.backend.service.VideoService;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
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
|
@Slf4j
|
||||||
@Tag(name = "视频管理", description = "视频相关接口")
|
@Tag(name = "视频管理", description = "视频相关接口")
|
||||||
|
@ -26,7 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
public class VideoController {
|
public class VideoController {
|
||||||
|
|
||||||
private final VideoService videoService;
|
private final VideoService videoService;
|
||||||
private final VideoRecommendService recommendService;
|
|
||||||
|
|
||||||
@Operation(summary = "上传视频", description = "上传视频文件并返回视频信息")
|
@Operation(summary = "上传视频", description = "上传视频文件并返回视频信息")
|
||||||
@SecurityRequirement(name = "bearer-jwt")
|
@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.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.dto.video.VideoDTO;
|
||||||
import com.guwan.backend.entity.Video;
|
import com.guwan.backend.entity.Video;
|
||||||
import com.guwan.backend.entity.VideoLike;
|
import com.guwan.backend.entity.VideoLike;
|
||||||
import com.guwan.backend.mapper.VideoMapper;
|
|
||||||
import com.guwan.backend.mapper.VideoLikeMapper;
|
import com.guwan.backend.mapper.VideoLikeMapper;
|
||||||
|
import com.guwan.backend.mapper.VideoMapper;
|
||||||
import com.guwan.backend.service.VideoService;
|
import com.guwan.backend.service.VideoService;
|
||||||
import com.guwan.backend.util.MinioUtil;
|
import com.guwan.backend.util.MinioUtil;
|
||||||
import com.guwan.backend.util.SecurityUtil;
|
import com.guwan.backend.util.SecurityUtil;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import io.minio.*;
|
||||||
import io.minio.http.Method;
|
import io.minio.http.Method;
|
||||||
import io.minio.messages.DeleteError;
|
import io.minio.messages.DeleteError;
|
||||||
import io.minio.messages.DeleteObject;
|
import io.minio.messages.DeleteObject;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
@ -17,7 +18,7 @@ import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
|
|
@ -5,6 +5,11 @@ spring:
|
||||||
application:
|
application:
|
||||||
name: backend
|
name: backend
|
||||||
|
|
||||||
|
# 视频上传配置
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 500MB
|
||||||
|
max-request-size: 500MB
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
@ -88,16 +93,9 @@ spring:
|
||||||
upload:
|
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配置
|
# Swagger配置
|
||||||
springdoc:
|
springdoc:
|
||||||
|
|
|
@ -18,20 +18,6 @@ CREATE TABLE IF NOT EXISTS `user` (
|
||||||
UNIQUE KEY `uk_phone` (`phone`)
|
UNIQUE KEY `uk_phone` (`phone`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
|
) 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` (
|
CREATE TABLE IF NOT EXISTS `sys_log` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||||
|
@ -51,3 +37,26 @@ CREATE TABLE IF NOT EXISTS `sys_log` (
|
||||||
KEY `idx_user_id` (`user_id`),
|
KEY `idx_user_id` (`user_id`),
|
||||||
KEY `idx_create_time` (`create_time`)
|
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