refactor(project): Reorganize frontend and backend directories
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
package com.nuaa.video.recommend;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 推荐服务启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients
|
||||
@EnableScheduling
|
||||
@MapperScan("com.nuaa.video.recommend.mapper")
|
||||
public class RecommendApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(RecommendApplication.class, args);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.nuaa.video.recommend.config;
|
||||
|
||||
import com.nuaa.video.recommend.filter.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* 安全配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/recommend/hot/**",
|
||||
"/recommend/tag/**",
|
||||
"/actuator/**",
|
||||
"/error"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package com.nuaa.video.recommend.controller;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.recommend.dto.UserBehaviorDTO;
|
||||
import com.nuaa.video.recommend.service.RecommendService;
|
||||
import com.nuaa.video.recommend.vo.VideoRecommendVO;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 推荐控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/recommend")
|
||||
@RequiredArgsConstructor
|
||||
public class RecommendController {
|
||||
|
||||
private final RecommendService recommendService;
|
||||
|
||||
/**
|
||||
* 获取首页推荐
|
||||
*/
|
||||
@GetMapping("/home")
|
||||
public Result<List<VideoRecommendVO>> getHomeRecommend(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "20") Integer size,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
return recommendService.getHomeRecommend(userId, page, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门推荐
|
||||
*/
|
||||
@GetMapping("/hot")
|
||||
public Result<List<VideoRecommendVO>> getHotRecommend(
|
||||
@RequestParam(value = "periodType", defaultValue = "1") Integer periodType,
|
||||
@RequestParam(value = "size", defaultValue = "20") Integer size) {
|
||||
return recommendService.getHotRecommend(periodType, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相似视频推荐
|
||||
*/
|
||||
@GetMapping("/similar/{videoId}")
|
||||
public Result<List<VideoRecommendVO>> getSimilarVideos(
|
||||
@PathVariable Long videoId,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size) {
|
||||
return recommendService.getSimilarVideos(videoId, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签视频推荐
|
||||
*/
|
||||
@GetMapping("/tag/{tagId}")
|
||||
public Result<List<VideoRecommendVO>> getTagVideos(
|
||||
@PathVariable Long tagId,
|
||||
@RequestParam(value = "size", defaultValue = "20") Integer size) {
|
||||
return recommendService.getTagVideos(tagId, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户行为
|
||||
*/
|
||||
@PostMapping("/behavior")
|
||||
public Result<Void> recordUserBehavior(
|
||||
@RequestBody @Valid UserBehaviorDTO behaviorDTO,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return recommendService.recordUserBehavior(userId, behaviorDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户兴趣
|
||||
*/
|
||||
@PostMapping("/interest/update")
|
||||
public Result<Void> updateUserInterests(HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return recommendService.updateUserInterests(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户ID
|
||||
*/
|
||||
private Long getUserIdFromRequest(HttpServletRequest request) {
|
||||
Object userIdObj = request.getAttribute("userId");
|
||||
if (userIdObj instanceof Long) {
|
||||
return (Long) userIdObj;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.nuaa.video.recommend.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户行为记录DTO
|
||||
*/
|
||||
@Data
|
||||
public class UserBehaviorDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
@NotNull(message = "视频ID不能为空")
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 行为类型(1:浏览,2:点赞,3:收藏,4:评论,5:分享,6:完整观看)
|
||||
*/
|
||||
@NotNull(message = "行为类型不能为空")
|
||||
private Integer behaviorType;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.nuaa.video.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 热门视频实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("recommend_hot_video")
|
||||
public class RecommendHotVideo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 热度分数
|
||||
*/
|
||||
private Float hotScore;
|
||||
|
||||
/**
|
||||
* 周期类型(1:天,2:周,3:月)
|
||||
*/
|
||||
private Integer periodType;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.nuaa.video.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户行为记录实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("recommend_user_behavior")
|
||||
public class RecommendUserBehavior implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 行为类型(1:浏览,2:点赞,3:收藏,4:评论,5:分享,6:完整观看)
|
||||
*/
|
||||
private Integer behaviorType;
|
||||
|
||||
/**
|
||||
* 行为权重
|
||||
*/
|
||||
private Float behaviorWeight;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.nuaa.video.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户兴趣标签实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("recommend_user_interest")
|
||||
public class RecommendUserInterest implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 标签ID
|
||||
*/
|
||||
private Long tagId;
|
||||
|
||||
/**
|
||||
* 权重
|
||||
*/
|
||||
private Float weight;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.nuaa.video.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户推荐实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("recommend_user_recommendation")
|
||||
public class RecommendUserRecommendation implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 推荐类型(1:基于内容,2:协同过滤,3:热门推荐,4:新视频推荐)
|
||||
*/
|
||||
private Integer recommendType;
|
||||
|
||||
/**
|
||||
* 推荐分数
|
||||
*/
|
||||
private Float score;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.nuaa.video.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 视频相似度实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("recommend_video_similarity")
|
||||
public class RecommendVideoSimilarity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 相似视频ID
|
||||
*/
|
||||
private Long similarVideoId;
|
||||
|
||||
/**
|
||||
* 相似度
|
||||
*/
|
||||
private Float similarity;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.nuaa.video.recommend.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 内容服务Feign客户端
|
||||
*/
|
||||
@FeignClient(name = "video-content", fallbackFactory = ContentFeignClientFallbackFactory.class)
|
||||
public interface ContentFeignClient {
|
||||
|
||||
/**
|
||||
* 获取视频详情
|
||||
*
|
||||
* @param id 视频ID
|
||||
* @return 视频信息
|
||||
*/
|
||||
@GetMapping("/content/video/{id}")
|
||||
Result<Object> getVideoInfo(@PathVariable("id") Long id);
|
||||
|
||||
/**
|
||||
* 获取视频列表
|
||||
*
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param categoryId 分类ID
|
||||
* @param tagIds 标签ID列表
|
||||
* @param orderBy 排序字段
|
||||
* @param orderDirection 排序方向
|
||||
* @param isRecommend 是否推荐
|
||||
* @return 视频列表
|
||||
*/
|
||||
@GetMapping("/content/video/list")
|
||||
Result<Object> getVideoList(
|
||||
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||
@RequestParam(value = "orderBy", required = false) String orderBy,
|
||||
@RequestParam(value = "orderDirection", required = false) String orderDirection,
|
||||
@RequestParam(value = "isRecommend", required = false) Integer isRecommend);
|
||||
|
||||
/**
|
||||
* 获取标签列表
|
||||
*
|
||||
* @return 标签列表
|
||||
*/
|
||||
@GetMapping("/content/tag/list")
|
||||
Result<Object> getTagList();
|
||||
|
||||
/**
|
||||
* 获取分类列表
|
||||
*
|
||||
* @return 分类列表
|
||||
*/
|
||||
@GetMapping("/content/category/list")
|
||||
Result<Object> getCategoryList();
|
||||
|
||||
/**
|
||||
* 获取视频标签
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 标签列表
|
||||
*/
|
||||
@GetMapping("/content/tag/video/{videoId}")
|
||||
Result<Object> getVideoTags(@PathVariable("videoId") Long videoId);
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.nuaa.video.recommend.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 内容服务Feign客户端降级工厂
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ContentFeignClientFallbackFactory implements FallbackFactory<ContentFeignClient> {
|
||||
|
||||
@Override
|
||||
public ContentFeignClient create(Throwable cause) {
|
||||
log.error("内容服务调用失败:", cause);
|
||||
return new ContentFeignClient() {
|
||||
@Override
|
||||
public Result<Object> getVideoInfo(Long id) {
|
||||
log.error("获取视频信息失败,视频ID:{}", id);
|
||||
return Result.error("获取视频信息失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Object> getVideoList(Integer pageNum, Integer pageSize, Long categoryId,
|
||||
List<Long> tagIds, String orderBy, String orderDirection, Integer isRecommend) {
|
||||
log.error("获取视频列表失败");
|
||||
return Result.error("获取视频列表失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Object> getTagList() {
|
||||
log.error("获取标签列表失败");
|
||||
return Result.error("获取标签列表失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Object> getCategoryList() {
|
||||
log.error("获取分类列表失败");
|
||||
return Result.error("获取分类列表失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Object> getVideoTags(Long videoId) {
|
||||
log.error("获取视频标签失败,视频ID:{}", videoId);
|
||||
return Result.error("获取视频标签失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.nuaa.video.recommend.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
/**
|
||||
* 用户服务Feign客户端
|
||||
*/
|
||||
@FeignClient(name = "video-user", fallbackFactory = UserFeignClientFallbackFactory.class)
|
||||
public interface UserFeignClient {
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
@GetMapping("/user/profile/public/{userId}")
|
||||
Result<Object> getUserProfile(@PathVariable("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*
|
||||
* @param token 认证令牌
|
||||
* @return 用户信息
|
||||
*/
|
||||
@GetMapping("/user/profile/current")
|
||||
Result<Object> getCurrentUserProfile(@RequestHeader("Authorization") String token);
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.nuaa.video.recommend.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 用户服务Feign客户端降级工厂
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
|
||||
|
||||
@Override
|
||||
public UserFeignClient create(Throwable cause) {
|
||||
log.error("用户服务调用失败:", cause);
|
||||
return new UserFeignClient() {
|
||||
@Override
|
||||
public Result<Object> getUserProfile(Long userId) {
|
||||
log.error("获取用户信息失败,用户ID:{}", userId);
|
||||
return Result.error("获取用户信息失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Object> getCurrentUserProfile(String token) {
|
||||
log.error("获取当前用户信息失败");
|
||||
return Result.error("获取当前用户信息失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.nuaa.video.recommend.filter;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
|
||||
private String secret;
|
||||
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 从请求头中获取JWT令牌
|
||||
String token = getJwtFromRequest(request);
|
||||
|
||||
// 如果令牌存在,则本地验证
|
||||
if (StringUtils.hasText(token)) {
|
||||
try {
|
||||
// 验证令牌
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
// 从令牌中提取用户ID和用户名
|
||||
Long userId = Long.parseLong(claims.getSubject());
|
||||
String username = claims.get("username", String.class);
|
||||
|
||||
// 设置用户权限
|
||||
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
// 创建认证对象
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(username, null, authorities);
|
||||
|
||||
// 设置额外信息
|
||||
authentication.setDetails(userId);
|
||||
|
||||
// 设置认证信息到安全上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
// 在请求属性中存储用户ID
|
||||
request.setAttribute("userId", userId);
|
||||
request.setAttribute("username", username);
|
||||
|
||||
log.debug("认证成功: 用户ID={}, 用户名={}", userId, username);
|
||||
} catch (Exception e) {
|
||||
log.error("JWT验证失败: {}", e.getMessage());
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取JWT令牌
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.nuaa.video.recommend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.recommend.entity.RecommendHotVideo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 热门视频Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RecommendHotVideoMapper extends BaseMapper<RecommendHotVideo> {
|
||||
|
||||
/**
|
||||
* 获取热门视频ID列表
|
||||
*
|
||||
* @param periodType 周期类型
|
||||
* @param limit 限制数量
|
||||
* @return 视频ID列表
|
||||
*/
|
||||
List<Long> selectHotVideoIds(
|
||||
@Param("periodType") Integer periodType,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 批量插入热门视频
|
||||
*
|
||||
* @param hotVideos 热门视频列表
|
||||
* @return 影响行数
|
||||
*/
|
||||
int batchInsert(@Param("hotVideos") List<RecommendHotVideo> hotVideos);
|
||||
|
||||
/**
|
||||
* 删除指定周期的热门视频
|
||||
*
|
||||
* @param periodType 周期类型
|
||||
* @return 影响行数
|
||||
*/
|
||||
int deleteByPeriodType(@Param("periodType") Integer periodType);
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.nuaa.video.recommend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserBehavior;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户行为记录Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RecommendUserBehaviorMapper extends BaseMapper<RecommendUserBehavior> {
|
||||
|
||||
/**
|
||||
* 获取用户行为记录的视频ID列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param behaviorTypes 行为类型列表
|
||||
* @param limit 限制数量
|
||||
* @return 视频ID列表
|
||||
*/
|
||||
List<Long> selectUserBehaviorVideoIds(
|
||||
@Param("userId") Long userId,
|
||||
@Param("behaviorTypes") List<Integer> behaviorTypes,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 统计用户分类行为数量
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param startTime 开始时间
|
||||
* @return 分类行为统计(categoryId -> count)
|
||||
*/
|
||||
List<Map<String, Object>> countUserCategoryBehaviors(
|
||||
@Param("userId") Long userId,
|
||||
@Param("startTime") LocalDateTime startTime);
|
||||
|
||||
/**
|
||||
* 统计用户标签行为数量
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param startTime 开始时间
|
||||
* @return 标签行为统计(tagId -> count)
|
||||
*/
|
||||
List<Map<String, Object>> countUserTagBehaviors(
|
||||
@Param("userId") Long userId,
|
||||
@Param("startTime") LocalDateTime startTime);
|
||||
|
||||
/**
|
||||
* 获取热门视频
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param behaviorTypes 行为类型列表
|
||||
* @param weights 行为权重Map(behaviorType -> weight)
|
||||
* @param limit 限制数量
|
||||
* @return 热门视频ID及分数
|
||||
*/
|
||||
List<Map<String, Object>> selectHotVideos(
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("behaviorTypes") List<Integer> behaviorTypes,
|
||||
@Param("weights") Map<Integer, Float> weights,
|
||||
@Param("limit") Integer limit);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.nuaa.video.recommend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserInterest;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户兴趣标签Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RecommendUserInterestMapper extends BaseMapper<RecommendUserInterest> {
|
||||
|
||||
/**
|
||||
* 获取用户标签权重前N的标签ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param limit 限制数量
|
||||
* @return 标签ID列表
|
||||
*/
|
||||
List<Long> selectTopTagIds(@Param("userId") Long userId, @Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 批量更新用户标签权重
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tagWeights 标签权重Map(tagId -> weight)
|
||||
* @return 影响行数
|
||||
*/
|
||||
int batchUpdateWeights(@Param("userId") Long userId, @Param("tagWeights") Map<Long, Float> tagWeights);
|
||||
|
||||
/**
|
||||
* 获取用户所有标签权重
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 标签权重Map(tagId -> weight)
|
||||
*/
|
||||
List<Map<String, Object>> selectUserTagWeights(@Param("userId") Long userId);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.nuaa.video.recommend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserRecommendation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户推荐Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RecommendUserRecommendationMapper extends BaseMapper<RecommendUserRecommendation> {
|
||||
|
||||
/**
|
||||
* 获取用户推荐视频ID列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param recommendType 推荐类型
|
||||
* @param limit 限制数量
|
||||
* @return 视频ID列表
|
||||
*/
|
||||
List<Long> selectUserRecommendVideoIds(
|
||||
@Param("userId") Long userId,
|
||||
@Param("recommendType") Integer recommendType,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 获取用户所有推荐视频ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 视频ID列表
|
||||
*/
|
||||
List<Long> selectAllUserRecommendVideoIds(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 批量插入用户推荐记录
|
||||
*
|
||||
* @param recommendations 推荐记录列表
|
||||
* @return 影响行数
|
||||
*/
|
||||
int batchInsert(@Param("recommendations") List<RecommendUserRecommendation> recommendations);
|
||||
|
||||
/**
|
||||
* 删除用户指定类型的推荐记录
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param recommendType 推荐类型
|
||||
* @return 影响行数
|
||||
*/
|
||||
int deleteUserRecommendations(
|
||||
@Param("userId") Long userId,
|
||||
@Param("recommendType") Integer recommendType);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.nuaa.video.recommend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.recommend.entity.RecommendVideoSimilarity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频相似度Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RecommendVideoSimilarityMapper extends BaseMapper<RecommendVideoSimilarity> {
|
||||
|
||||
/**
|
||||
* 获取相似视频列表
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @param threshold 相似度阈值
|
||||
* @param limit 限制数量
|
||||
* @return 相似视频ID列表
|
||||
*/
|
||||
List<Long> selectSimilarVideoIds(
|
||||
@Param("videoId") Long videoId,
|
||||
@Param("threshold") Float threshold,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 批量插入或更新视频相似度
|
||||
*
|
||||
* @param similarities 视频相似度列表
|
||||
* @return 影响行数
|
||||
*/
|
||||
int batchInsertOrUpdate(@Param("similarities") List<RecommendVideoSimilarity> similarities);
|
||||
|
||||
/**
|
||||
* 获取视频相似度TOP N
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @param limit 限制数量
|
||||
* @return 相似视频及分数
|
||||
*/
|
||||
List<Map<String, Object>> selectTopSimilarVideos(
|
||||
@Param("videoId") Long videoId,
|
||||
@Param("limit") Integer limit);
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.nuaa.video.recommend.service;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.recommend.dto.UserBehaviorDTO;
|
||||
import com.nuaa.video.recommend.vo.VideoRecommendVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 推荐服务接口
|
||||
*/
|
||||
public interface RecommendService {
|
||||
|
||||
/**
|
||||
* 获取首页推荐
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
* @return 推荐视频列表
|
||||
*/
|
||||
Result<List<VideoRecommendVO>> getHomeRecommend(Long userId, Integer page, Integer size);
|
||||
|
||||
/**
|
||||
* 获取热门推荐
|
||||
*
|
||||
* @param periodType 周期类型(1:天,2:周,3:月)
|
||||
* @param size 数量
|
||||
* @return 热门视频列表
|
||||
*/
|
||||
Result<List<VideoRecommendVO>> getHotRecommend(Integer periodType, Integer size);
|
||||
|
||||
/**
|
||||
* 获取相似视频推荐
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @param size 数量
|
||||
* @return 相似视频列表
|
||||
*/
|
||||
Result<List<VideoRecommendVO>> getSimilarVideos(Long videoId, Integer size);
|
||||
|
||||
/**
|
||||
* 获取标签视频推荐
|
||||
*
|
||||
* @param tagId 标签ID
|
||||
* @param size 数量
|
||||
* @return 标签相关视频列表
|
||||
*/
|
||||
Result<List<VideoRecommendVO>> getTagVideos(Long tagId, Integer size);
|
||||
|
||||
/**
|
||||
* 记录用户行为
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param behaviorDTO 行为数据
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> recordUserBehavior(Long userId, UserBehaviorDTO behaviorDTO);
|
||||
|
||||
/**
|
||||
* 更新用户兴趣
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> updateUserInterests(Long userId);
|
||||
}
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
package com.nuaa.video.recommend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.recommend.dto.UserBehaviorDTO;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserBehavior;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserInterest;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserRecommendation;
|
||||
import com.nuaa.video.recommend.entity.RecommendVideoSimilarity;
|
||||
import com.nuaa.video.recommend.feign.ContentFeignClient;
|
||||
import com.nuaa.video.recommend.mapper.RecommendUserBehaviorMapper;
|
||||
import com.nuaa.video.recommend.mapper.RecommendUserInterestMapper;
|
||||
import com.nuaa.video.recommend.mapper.RecommendUserRecommendationMapper;
|
||||
import com.nuaa.video.recommend.mapper.RecommendVideoSimilarityMapper;
|
||||
import com.nuaa.video.recommend.service.RecommendService;
|
||||
import com.nuaa.video.recommend.strategy.RecommendStrategy;
|
||||
import com.nuaa.video.recommend.strategy.RecommendStrategyFactory;
|
||||
import com.nuaa.video.recommend.vo.VideoRecommendVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 推荐服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RecommendServiceImpl implements RecommendService {
|
||||
|
||||
private final RecommendUserBehaviorMapper userBehaviorMapper;
|
||||
private final RecommendUserInterestMapper userInterestMapper;
|
||||
private final RecommendUserRecommendationMapper userRecommendationMapper;
|
||||
private final RecommendVideoSimilarityMapper videoSimilarityMapper;
|
||||
private final ContentFeignClient contentFeignClient;
|
||||
private final RecommendStrategyFactory strategyFactory;
|
||||
|
||||
@Override
|
||||
public Result<List<VideoRecommendVO>> getHomeRecommend(Long userId, Integer page, Integer size) {
|
||||
if (page == null || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 20;
|
||||
}
|
||||
|
||||
List<VideoRecommendVO> recommendList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 不同页使用不同的推荐策略,提供多样性
|
||||
if (userId == null) {
|
||||
// 游客只使用热门推荐
|
||||
RecommendStrategy hotStrategy = strategyFactory.getStrategy(3);
|
||||
recommendList = hotStrategy.recommendForGuest(size);
|
||||
} else {
|
||||
int pageType = page % 3; // 分成3种页面类型
|
||||
|
||||
if (pageType == 0) {
|
||||
// 页码被3整除,使用热门推荐
|
||||
RecommendStrategy hotStrategy = strategyFactory.getStrategy(3);
|
||||
recommendList = hotStrategy.recommend(userId, size);
|
||||
} else if (pageType == 1) {
|
||||
// 页码模3余1,使用基于内容的推荐
|
||||
RecommendStrategy contentStrategy = strategyFactory.getStrategy(1);
|
||||
recommendList = contentStrategy.recommend(userId, size);
|
||||
|
||||
// 如果基于内容的推荐不足,使用热门推荐补充
|
||||
if (recommendList.size() < size) {
|
||||
RecommendStrategy hotStrategy = strategyFactory.getStrategy(3);
|
||||
List<VideoRecommendVO> hotRecommends = hotStrategy.recommend(userId, size - recommendList.size());
|
||||
recommendList.addAll(hotRecommends);
|
||||
}
|
||||
} else {
|
||||
// 页码模3余2,混合推荐
|
||||
RecommendStrategy contentStrategy = strategyFactory.getStrategy(1);
|
||||
List<VideoRecommendVO> contentRecommends = contentStrategy.recommend(userId, size / 2);
|
||||
|
||||
RecommendStrategy hotStrategy = strategyFactory.getStrategy(3);
|
||||
List<VideoRecommendVO> hotRecommends = hotStrategy.recommend(userId, size - contentRecommends.size());
|
||||
|
||||
recommendList.addAll(contentRecommends);
|
||||
recommendList.addAll(hotRecommends);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取首页推荐异常, userId={}, page={}, size={}", userId, page, size, e);
|
||||
}
|
||||
|
||||
return Result.success(recommendList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<VideoRecommendVO>> getHotRecommend(Integer periodType, Integer size) {
|
||||
if (periodType == null || periodType < 1 || periodType > 3) {
|
||||
periodType = 1; // 默认使用日榜
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 20;
|
||||
}
|
||||
|
||||
List<VideoRecommendVO> recommendList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
RecommendStrategy hotStrategy = strategyFactory.getStrategy(3);
|
||||
recommendList = hotStrategy.recommendForGuest(size);
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门推荐异常, periodType={}, size={}", periodType, size, e);
|
||||
}
|
||||
|
||||
return Result.success(recommendList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<VideoRecommendVO>> getSimilarVideos(Long videoId, Integer size) {
|
||||
if (videoId == null) {
|
||||
return Result.error("视频ID不能为空");
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
List<VideoRecommendVO> recommendList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 从数据库获取相似视频
|
||||
List<Map<String, Object>> similarVideos = videoSimilarityMapper.selectTopSimilarVideos(videoId, size);
|
||||
|
||||
if (similarVideos.isEmpty()) {
|
||||
// 如果没有相似视频记录,直接通过内容服务获取相同分类的视频
|
||||
Result<Object> videoResult = contentFeignClient.getVideoInfo(videoId);
|
||||
if (videoResult.getCode() == 200 && videoResult.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) videoResult.getData();
|
||||
|
||||
Long categoryId = null;
|
||||
if (videoData.containsKey("categoryId")) {
|
||||
categoryId = Long.valueOf(videoData.get("categoryId").toString());
|
||||
}
|
||||
|
||||
if (categoryId != null) {
|
||||
Result<Object> result = contentFeignClient.getVideoList(1, size, categoryId, null,
|
||||
"view_count", "desc", null);
|
||||
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> pageData = (Map<String, Object>) result.getData();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> records = (List<Map<String, Object>>) pageData.get("records");
|
||||
|
||||
if (records != null) {
|
||||
for (Map<String, Object> video : records) {
|
||||
Long id = Long.valueOf(video.get("id").toString());
|
||||
if (!id.equals(videoId)) { // 过滤掉当前视频
|
||||
VideoRecommendVO vo = mapToRecommendVO(video);
|
||||
vo.setRecommendReason("相关推荐");
|
||||
recommendList.add(vo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 有相似视频记录,获取视频详情
|
||||
for (Map<String, Object> similar : similarVideos) {
|
||||
Long similarVideoId = Long.valueOf(similar.get("videoId").toString());
|
||||
Result<Object> result = contentFeignClient.getVideoInfo(similarVideoId);
|
||||
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) result.getData();
|
||||
|
||||
VideoRecommendVO vo = mapToRecommendVO(videoData);
|
||||
|
||||
// 设置推荐原因
|
||||
String reason = "相关推荐";
|
||||
if (vo.getTagNames() != null && !vo.getTagNames().isEmpty()) {
|
||||
reason = "与本视频相关: " + vo.getTagNames().get(0);
|
||||
}
|
||||
vo.setRecommendReason(reason);
|
||||
|
||||
recommendList.add(vo);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取相似视频推荐异常, videoId={}, size={}", videoId, size, e);
|
||||
}
|
||||
|
||||
return Result.success(recommendList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<VideoRecommendVO>> getTagVideos(Long tagId, Integer size) {
|
||||
if (tagId == null) {
|
||||
return Result.error("标签ID不能为空");
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 20;
|
||||
}
|
||||
|
||||
List<VideoRecommendVO> recommendList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 通过内容服务获取指定标签的视频
|
||||
List<Long> tagIds = Collections.singletonList(tagId);
|
||||
Result<Object> result = contentFeignClient.getVideoList(1, size, null, tagIds,
|
||||
"view_count", "desc", null);
|
||||
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> pageData = (Map<String, Object>) result.getData();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> records = (List<Map<String, Object>>) pageData.get("records");
|
||||
|
||||
if (records != null) {
|
||||
for (Map<String, Object> video : records) {
|
||||
VideoRecommendVO vo = mapToRecommendVO(video);
|
||||
|
||||
// 获取标签名称
|
||||
String tagName = "相关内容";
|
||||
if (vo.getTagNames() != null && !vo.getTagNames().isEmpty()) {
|
||||
for (String name : vo.getTagNames()) {
|
||||
tagName = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
vo.setRecommendReason("相关标签: " + tagName);
|
||||
recommendList.add(vo);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取标签视频推荐异常, tagId={}, size={}", tagId, size, e);
|
||||
}
|
||||
|
||||
return Result.success(recommendList);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> recordUserBehavior(Long userId, UserBehaviorDTO behaviorDTO) {
|
||||
if (userId == null) {
|
||||
return Result.error("用户ID不能为空");
|
||||
}
|
||||
if (behaviorDTO == null || behaviorDTO.getVideoId() == null || behaviorDTO.getBehaviorType() == null) {
|
||||
return Result.error("行为数据不完整");
|
||||
}
|
||||
|
||||
try {
|
||||
// 根据行为类型设置权重
|
||||
float weight = 1.0f;
|
||||
switch (behaviorDTO.getBehaviorType()) {
|
||||
case 1: // 浏览
|
||||
weight = 1.0f;
|
||||
break;
|
||||
case 2: // 点赞
|
||||
weight = 3.0f;
|
||||
break;
|
||||
case 3: // 收藏
|
||||
weight = 5.0f;
|
||||
break;
|
||||
case 4: // 评论
|
||||
weight = 2.0f;
|
||||
break;
|
||||
case 5: // 分享
|
||||
weight = 3.0f;
|
||||
break;
|
||||
case 6: // 完整观看
|
||||
weight = 4.0f;
|
||||
break;
|
||||
default:
|
||||
weight = 1.0f;
|
||||
}
|
||||
|
||||
// 记录用户行为
|
||||
RecommendUserBehavior behavior = new RecommendUserBehavior();
|
||||
behavior.setUserId(userId);
|
||||
behavior.setVideoId(behaviorDTO.getVideoId());
|
||||
behavior.setBehaviorType(behaviorDTO.getBehaviorType());
|
||||
behavior.setBehaviorWeight(weight);
|
||||
behavior.setCreatedBy(userId.toString());
|
||||
behavior.setCreateTime(LocalDateTime.now());
|
||||
|
||||
userBehaviorMapper.insert(behavior);
|
||||
|
||||
// 异步更新用户兴趣(实际项目中可以使用消息队列或线程池)
|
||||
updateUserInterests(userId);
|
||||
|
||||
return Result.success();
|
||||
} catch (Exception e) {
|
||||
log.error("记录用户行为异常, userId={}, behaviorDTO={}", userId, behaviorDTO, e);
|
||||
return Result.error("记录用户行为失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> updateUserInterests(Long userId) {
|
||||
if (userId == null) {
|
||||
return Result.error("用户ID不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户最近30天的行为
|
||||
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
|
||||
|
||||
// 1. 统计标签行为数量
|
||||
List<Map<String, Object>> tagBehaviors = userBehaviorMapper.countUserTagBehaviors(userId, startTime);
|
||||
|
||||
if (!tagBehaviors.isEmpty()) {
|
||||
// 计算标签总权重
|
||||
double totalWeight = tagBehaviors.stream()
|
||||
.mapToDouble(tag -> Double.parseDouble(tag.get("count").toString()))
|
||||
.sum();
|
||||
|
||||
// 计算标签权重
|
||||
Map<Long, Float> tagWeights = new HashMap<>();
|
||||
for (Map<String, Object> tag : tagBehaviors) {
|
||||
Long tagId = Long.valueOf(tag.get("tagId").toString());
|
||||
double count = Double.parseDouble(tag.get("count").toString());
|
||||
float weight = (float) (count / totalWeight);
|
||||
tagWeights.put(tagId, weight);
|
||||
}
|
||||
|
||||
// 更新用户标签兴趣
|
||||
userInterestMapper.batchUpdateWeights(userId, tagWeights);
|
||||
}
|
||||
|
||||
// 2. 统计分类行为数量(如果需要可以添加分类兴趣统计)
|
||||
|
||||
return Result.success();
|
||||
} catch (Exception e) {
|
||||
log.error("更新用户兴趣异常, userId={}", userId, e);
|
||||
return Result.error("更新用户兴趣失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视频数据映射为推荐VO
|
||||
*/
|
||||
private VideoRecommendVO mapToRecommendVO(Map<String, Object> videoData) {
|
||||
VideoRecommendVO vo = new VideoRecommendVO();
|
||||
|
||||
// 设置基本信息
|
||||
if (videoData.containsKey("id")) vo.setId(Long.valueOf(videoData.get("id").toString()));
|
||||
if (videoData.containsKey("title")) vo.setTitle((String) videoData.get("title"));
|
||||
if (videoData.containsKey("description")) vo.setDescription((String) videoData.get("description"));
|
||||
if (videoData.containsKey("coverUrl")) vo.setCoverUrl((String) videoData.get("coverUrl"));
|
||||
if (videoData.containsKey("duration")) vo.setDuration(Integer.valueOf(videoData.get("duration").toString()));
|
||||
if (videoData.containsKey("userId")) vo.setUserId(Long.valueOf(videoData.get("userId").toString()));
|
||||
if (videoData.containsKey("userNickname")) vo.setUserNickname((String) videoData.get("userNickname"));
|
||||
if (videoData.containsKey("userAvatar")) vo.setUserAvatar((String) videoData.get("userAvatar"));
|
||||
if (videoData.containsKey("categoryId")) vo.setCategoryId(Long.valueOf(videoData.get("categoryId").toString()));
|
||||
if (videoData.containsKey("categoryName")) vo.setCategoryName((String) videoData.get("categoryName"));
|
||||
if (videoData.containsKey("likeCount")) vo.setLikeCount(Integer.valueOf(videoData.get("likeCount").toString()));
|
||||
if (videoData.containsKey("commentCount")) vo.setCommentCount(Integer.valueOf(videoData.get("commentCount").toString()));
|
||||
if (videoData.containsKey("viewCount")) vo.setViewCount(Long.valueOf(videoData.get("viewCount").toString()));
|
||||
|
||||
// 设置标签
|
||||
if (videoData.containsKey("tagNames")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> tagNames = (List<String>) videoData.get("tagNames");
|
||||
vo.setTagNames(tagNames);
|
||||
}
|
||||
|
||||
// 设置发布时间
|
||||
if (videoData.containsKey("publishTime")) {
|
||||
String publishTimeStr = videoData.get("publishTime").toString();
|
||||
vo.setPublishTime(LocalDateTime.parse(publishTimeStr));
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package com.nuaa.video.recommend.strategy;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.recommend.entity.RecommendUserInterest;
|
||||
import com.nuaa.video.recommend.entity.RecommendVideoSimilarity;
|
||||
import com.nuaa.video.recommend.feign.ContentFeignClient;
|
||||
import com.nuaa.video.recommend.mapper.RecommendUserBehaviorMapper;
|
||||
import com.nuaa.video.recommend.mapper.RecommendUserInterestMapper;
|
||||
import com.nuaa.video.recommend.mapper.RecommendVideoSimilarityMapper;
|
||||
import com.nuaa.video.recommend.vo.VideoRecommendVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 基于内容的推荐策略
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ContentBasedRecommendStrategy implements RecommendStrategy {
|
||||
|
||||
private final RecommendUserInterestMapper userInterestMapper;
|
||||
private final RecommendUserBehaviorMapper userBehaviorMapper;
|
||||
private final RecommendVideoSimilarityMapper videoSimilarityMapper;
|
||||
private final ContentFeignClient contentFeignClient;
|
||||
|
||||
/**
|
||||
* 基于内容的推荐类型: 1
|
||||
*/
|
||||
private static final Integer CONTENT_BASED_RECOMMEND_TYPE = 1;
|
||||
|
||||
@Override
|
||||
public Integer getType() {
|
||||
return CONTENT_BASED_RECOMMEND_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoRecommendVO> recommend(Long userId, Integer size) {
|
||||
if (size == null || size <= 0) {
|
||||
size = 20;
|
||||
}
|
||||
|
||||
// 根据用户兴趣推荐
|
||||
List<VideoRecommendVO> recommendList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 1. 获取用户的兴趣标签
|
||||
List<Map<String, Object>> userTagWeights = userInterestMapper.selectUserTagWeights(userId);
|
||||
|
||||
if (userTagWeights == null || userTagWeights.isEmpty()) {
|
||||
// 如果用户没有兴趣标签,则获取用户最近的行为
|
||||
List<Integer> behaviorTypes = Arrays.asList(2, 3, 4, 5, 6); // 点赞、收藏、评论、分享、完整观看
|
||||
List<Long> recentVideoIds = userBehaviorMapper.selectUserBehaviorVideoIds(userId, behaviorTypes, 10);
|
||||
|
||||
if (recentVideoIds.isEmpty()) {
|
||||
// 如果用户没有行为记录,则返回空列表,后续会由热门推荐补充
|
||||
return recommendList;
|
||||
}
|
||||
|
||||
// 基于用户最近行为的视频,找相似视频
|
||||
Map<Long, Double> candidateVideos = new HashMap<>();
|
||||
|
||||
for (Long videoId : recentVideoIds) {
|
||||
List<Map<String, Object>> similarVideos = videoSimilarityMapper.selectTopSimilarVideos(videoId, size / 2);
|
||||
for (Map<String, Object> video : similarVideos) {
|
||||
Long similarVideoId = Long.valueOf(video.get("videoId").toString());
|
||||
Double similarity = Double.valueOf(video.get("score").toString());
|
||||
|
||||
// 过滤掉用户已经看过的视频
|
||||
if (!recentVideoIds.contains(similarVideoId)) {
|
||||
candidateVideos.put(similarVideoId,
|
||||
candidateVideos.getOrDefault(similarVideoId, 0.0) + similarity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按相似度降序排序
|
||||
List<Map.Entry<Long, Double>> sortedCandidates = new ArrayList<>(candidateVideos.entrySet());
|
||||
sortedCandidates.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue()));
|
||||
|
||||
// 取前N个
|
||||
List<Long> recommendIds = sortedCandidates.stream()
|
||||
.limit(size)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取视频详情
|
||||
for (Long videoId : recommendIds) {
|
||||
Result<Object> result = contentFeignClient.getVideoInfo(videoId);
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) result.getData();
|
||||
|
||||
VideoRecommendVO vo = mapToRecommendVO(videoData);
|
||||
vo.setRecommendReason("根据您的观看历史推荐");
|
||||
recommendList.add(vo);
|
||||
}
|
||||
}
|
||||
|
||||
return recommendList;
|
||||
}
|
||||
|
||||
// 2. 用户有兴趣标签,基于标签推荐
|
||||
Map<Long, Float> tagWeightMap = new HashMap<>();
|
||||
for (Map<String, Object> weight : userTagWeights) {
|
||||
tagWeightMap.put(Long.valueOf(weight.get("tag_id").toString()),
|
||||
Float.valueOf(weight.get("weight").toString()));
|
||||
}
|
||||
|
||||
// 3. 获取带权重的推荐标签ID
|
||||
List<Long> weightedTagIds = new ArrayList<>();
|
||||
for (Map.Entry<Long, Float> entry : tagWeightMap.entrySet()) {
|
||||
int count = Math.round(entry.getValue() * 10); // 标签权重决定在推荐池中的数量
|
||||
for (int i = 0; i < count; i++) {
|
||||
weightedTagIds.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// 随机选择标签,避免推荐结果过于单一
|
||||
Collections.shuffle(weightedTagIds);
|
||||
|
||||
// 4. 根据标签获取视频列表
|
||||
Set<Long> recommendedVideoIds = new HashSet<>();
|
||||
for (Long tagId : weightedTagIds) {
|
||||
if (recommendList.size() >= size) break;
|
||||
|
||||
// 根据标签ID获取相关视频
|
||||
List<Long> tagIds = Collections.singletonList(tagId);
|
||||
Result<Object> result = contentFeignClient.getVideoList(1, 5, null, tagIds,
|
||||
"view_count", "desc", null);
|
||||
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> pageData = (Map<String, Object>) result.getData();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> records = (List<Map<String, Object>>) pageData.get("records");
|
||||
|
||||
if (records != null) {
|
||||
for (Map<String, Object> video : records) {
|
||||
Long videoId = Long.valueOf(video.get("id").toString());
|
||||
|
||||
// 过滤掉已经推荐的视频
|
||||
if (!recommendedVideoIds.contains(videoId)) {
|
||||
recommendedVideoIds.add(videoId);
|
||||
|
||||
VideoRecommendVO vo = mapToRecommendVO(video);
|
||||
|
||||
// 设置推荐原因
|
||||
String tagName = "您感兴趣的内容";
|
||||
if (video.containsKey("tagNames") && ((List<String>) video.get("tagNames")).size() > 0) {
|
||||
tagName = ((List<String>) video.get("tagNames")).get(0);
|
||||
}
|
||||
vo.setRecommendReason("根据您对" + tagName + "的兴趣推荐");
|
||||
|
||||
recommendList.add(vo);
|
||||
|
||||
if (recommendList.size() >= size) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("基于内容的推荐异常, userId={}", userId, e);
|
||||
}
|
||||
|
||||
return recommendList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoRecommendVO> recommendForGuest(Integer size) {
|
||||
// 游客没有兴趣标签,返回空列表,后续会由热门推荐补充
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "基于内容的推荐";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视频数据映射为推荐VO
|
||||
*/
|
||||
private VideoRecommendVO mapToRecommendVO(Map<String, Object> videoData) {
|
||||
VideoRecommendVO vo = new VideoRecommendVO();
|
||||
|
||||
// 设置基本信息
|
||||
if (videoData.containsKey("id")) vo.setId(Long.valueOf(videoData.get("id").toString()));
|
||||
if (videoData.containsKey("title")) vo.setTitle((String) videoData.get("title"));
|
||||
if (videoData.containsKey("description")) vo.setDescription((String) videoData.get("description"));
|
||||
if (videoData.containsKey("coverUrl")) vo.setCoverUrl((String) videoData.get("coverUrl"));
|
||||
if (videoData.containsKey("duration")) vo.setDuration(Integer.valueOf(videoData.get("duration").toString()));
|
||||
if (videoData.containsKey("userId")) vo.setUserId(Long.valueOf(videoData.get("userId").toString()));
|
||||
if (videoData.containsKey("userNickname")) vo.setUserNickname((String) videoData.get("userNickname"));
|
||||
if (videoData.containsKey("userAvatar")) vo.setUserAvatar((String) videoData.get("userAvatar"));
|
||||
if (videoData.containsKey("categoryId")) vo.setCategoryId(Long.valueOf(videoData.get("categoryId").toString()));
|
||||
if (videoData.containsKey("categoryName")) vo.setCategoryName((String) videoData.get("categoryName"));
|
||||
if (videoData.containsKey("likeCount")) vo.setLikeCount(Integer.valueOf(videoData.get("likeCount").toString()));
|
||||
if (videoData.containsKey("commentCount")) vo.setCommentCount(Integer.valueOf(videoData.get("commentCount").toString()));
|
||||
if (videoData.containsKey("viewCount")) vo.setViewCount(Long.valueOf(videoData.get("viewCount").toString()));
|
||||
|
||||
// 设置标签
|
||||
if (videoData.containsKey("tagNames")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> tagNames = (List<String>) videoData.get("tagNames");
|
||||
vo.setTagNames(tagNames);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.nuaa.video.recommend.strategy;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.recommend.feign.ContentFeignClient;
|
||||
import com.nuaa.video.recommend.mapper.RecommendHotVideoMapper;
|
||||
import com.nuaa.video.recommend.vo.VideoRecommendVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 热门推荐策略
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HotRecommendStrategy implements RecommendStrategy {
|
||||
|
||||
private final RecommendHotVideoMapper hotVideoMapper;
|
||||
private final ContentFeignClient contentFeignClient;
|
||||
|
||||
@Value("${recommend.hot.default-size:20}")
|
||||
private Integer defaultSize;
|
||||
|
||||
/**
|
||||
* 热门推荐类型: 3
|
||||
*/
|
||||
private static final Integer HOT_RECOMMEND_TYPE = 3;
|
||||
|
||||
@Override
|
||||
public Integer getType() {
|
||||
return HOT_RECOMMEND_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoRecommendVO> recommend(Long userId, Integer size) {
|
||||
// 对已登录用户的热门推荐与游客相同,但可以过滤掉已经看过的视频
|
||||
return recommendForGuest(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoRecommendVO> recommendForGuest(Integer size) {
|
||||
if (size == null || size <= 0) {
|
||||
size = defaultSize;
|
||||
}
|
||||
|
||||
// 获取每日热门视频
|
||||
List<Long> hotVideoIds = hotVideoMapper.selectHotVideoIds(1, size);
|
||||
|
||||
// 如果每日热门不足,则获取每周热门补充
|
||||
if (hotVideoIds.size() < size) {
|
||||
List<Long> weeklyHotVideoIds = hotVideoMapper.selectHotVideoIds(2, size - hotVideoIds.size());
|
||||
hotVideoIds.addAll(weeklyHotVideoIds);
|
||||
}
|
||||
|
||||
// 如果仍然不足,则获取每月热门补充
|
||||
if (hotVideoIds.size() < size) {
|
||||
List<Long> monthlyHotVideoIds = hotVideoMapper.selectHotVideoIds(3, size - hotVideoIds.size());
|
||||
hotVideoIds.addAll(monthlyHotVideoIds);
|
||||
}
|
||||
|
||||
// 调用内容服务获取视频详情
|
||||
List<VideoRecommendVO> recommendList = new ArrayList<>();
|
||||
for (Long videoId : hotVideoIds) {
|
||||
try {
|
||||
Result<Object> result = contentFeignClient.getVideoInfo(videoId);
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) result.getData();
|
||||
|
||||
VideoRecommendVO vo = mapToRecommendVO(videoData);
|
||||
vo.setRecommendReason("热门推荐");
|
||||
recommendList.add(vo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取视频信息失败, videoId={}", videoId, e);
|
||||
}
|
||||
}
|
||||
|
||||
return recommendList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "热门推荐";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视频数据映射为推荐VO
|
||||
*/
|
||||
private VideoRecommendVO mapToRecommendVO(Map<String, Object> videoData) {
|
||||
VideoRecommendVO vo = new VideoRecommendVO();
|
||||
|
||||
// 设置基本信息
|
||||
if (videoData.containsKey("id")) vo.setId(Long.valueOf(videoData.get("id").toString()));
|
||||
if (videoData.containsKey("title")) vo.setTitle((String) videoData.get("title"));
|
||||
if (videoData.containsKey("description")) vo.setDescription((String) videoData.get("description"));
|
||||
if (videoData.containsKey("coverUrl")) vo.setCoverUrl((String) videoData.get("coverUrl"));
|
||||
if (videoData.containsKey("duration")) vo.setDuration(Integer.valueOf(videoData.get("duration").toString()));
|
||||
if (videoData.containsKey("userId")) vo.setUserId(Long.valueOf(videoData.get("userId").toString()));
|
||||
if (videoData.containsKey("userNickname")) vo.setUserNickname((String) videoData.get("userNickname"));
|
||||
if (videoData.containsKey("userAvatar")) vo.setUserAvatar((String) videoData.get("userAvatar"));
|
||||
if (videoData.containsKey("categoryId")) vo.setCategoryId(Long.valueOf(videoData.get("categoryId").toString()));
|
||||
if (videoData.containsKey("categoryName")) vo.setCategoryName((String) videoData.get("categoryName"));
|
||||
if (videoData.containsKey("likeCount")) vo.setLikeCount(Integer.valueOf(videoData.get("likeCount").toString()));
|
||||
if (videoData.containsKey("commentCount")) vo.setCommentCount(Integer.valueOf(videoData.get("commentCount").toString()));
|
||||
if (videoData.containsKey("viewCount")) vo.setViewCount(Long.valueOf(videoData.get("viewCount").toString()));
|
||||
|
||||
// 设置标签
|
||||
if (videoData.containsKey("tagNames")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> tagNames = (List<String>) videoData.get("tagNames");
|
||||
vo.setTagNames(tagNames);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.nuaa.video.recommend.strategy;
|
||||
|
||||
import com.nuaa.video.recommend.vo.VideoRecommendVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 推荐策略接口
|
||||
*/
|
||||
public interface RecommendStrategy {
|
||||
|
||||
/**
|
||||
* 获取推荐策略类型
|
||||
*
|
||||
* @return 推荐类型
|
||||
*/
|
||||
Integer getType();
|
||||
|
||||
/**
|
||||
* 为用户推荐视频
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param size 推荐数量
|
||||
* @return 推荐视频列表
|
||||
*/
|
||||
List<VideoRecommendVO> recommend(Long userId, Integer size);
|
||||
|
||||
/**
|
||||
* 为游客推荐视频
|
||||
*
|
||||
* @param size 推荐数量
|
||||
* @return 推荐视频列表
|
||||
*/
|
||||
List<VideoRecommendVO> recommendForGuest(Integer size);
|
||||
|
||||
/**
|
||||
* 获取策略名称
|
||||
*
|
||||
* @return 策略名称
|
||||
*/
|
||||
String getName();
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.nuaa.video.recommend.strategy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 推荐策略工厂
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RecommendStrategyFactory {
|
||||
|
||||
@Autowired
|
||||
private List<RecommendStrategy> strategies;
|
||||
|
||||
private final Map<Integer, RecommendStrategy> strategyMap = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
strategies.forEach(strategy -> {
|
||||
strategyMap.put(strategy.getType(), strategy);
|
||||
log.info("注册推荐策略: type={}, name={}", strategy.getType(), strategy.getName());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐策略
|
||||
*
|
||||
* @param type 推荐类型
|
||||
* @return 推荐策略
|
||||
*/
|
||||
public RecommendStrategy getStrategy(Integer type) {
|
||||
RecommendStrategy strategy = strategyMap.get(type);
|
||||
if (strategy == null) {
|
||||
log.warn("未找到推荐策略: type={}, 使用热门推荐策略", type);
|
||||
strategy = strategyMap.get(3); // 默认使用热门推荐
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有推荐策略
|
||||
*
|
||||
* @return 推荐策略Map
|
||||
*/
|
||||
public Map<Integer, RecommendStrategy> getAllStrategies() {
|
||||
return strategyMap;
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.nuaa.video.recommend.task;
|
||||
|
||||
import com.nuaa.video.recommend.entity.RecommendHotVideo;
|
||||
import com.nuaa.video.recommend.mapper.RecommendHotVideoMapper;
|
||||
import com.nuaa.video.recommend.mapper.RecommendUserBehaviorMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 热门视频更新定时任务
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HotVideoUpdateTask {
|
||||
|
||||
private final RecommendUserBehaviorMapper userBehaviorMapper;
|
||||
private final RecommendHotVideoMapper hotVideoMapper;
|
||||
|
||||
@Value("${recommend.hot.weights.view:1}")
|
||||
private float viewWeight;
|
||||
|
||||
@Value("${recommend.hot.weights.like:3}")
|
||||
private float likeWeight;
|
||||
|
||||
@Value("${recommend.hot.weights.favorite:5}")
|
||||
private float favoriteWeight;
|
||||
|
||||
@Value("${recommend.hot.weights.coin:5}")
|
||||
private float coinWeight;
|
||||
|
||||
@Value("${recommend.hot.weights.comment:2}")
|
||||
private float commentWeight;
|
||||
|
||||
@Value("${recommend.hot.weights.share:3}")
|
||||
private float shareWeight;
|
||||
|
||||
/**
|
||||
* 每小时更新日榜热门视频
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateDailyHotVideos() {
|
||||
log.info("开始更新日榜热门视频");
|
||||
try {
|
||||
LocalDateTime startTime = LocalDateTime.now().minusDays(1);
|
||||
updateHotVideos(startTime, 1, 100);
|
||||
log.info("日榜热门视频更新完成");
|
||||
} catch (Exception e) {
|
||||
log.error("更新日榜热门视频失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天凌晨2点更新周榜热门视频
|
||||
*/
|
||||
@Scheduled(cron = "0 0 2 * * ?")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateWeeklyHotVideos() {
|
||||
log.info("开始更新周榜热门视频");
|
||||
try {
|
||||
LocalDateTime startTime = LocalDateTime.now().minusDays(7);
|
||||
updateHotVideos(startTime, 2, 100);
|
||||
log.info("周榜热门视频更新完成");
|
||||
} catch (Exception e) {
|
||||
log.error("更新周榜热门视频失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每周一凌晨3点更新月榜热门视频
|
||||
*/
|
||||
@Scheduled(cron = "0 0 3 ? * MON")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateMonthlyHotVideos() {
|
||||
log.info("开始更新月榜热门视频");
|
||||
try {
|
||||
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
|
||||
updateHotVideos(startTime, 3, 100);
|
||||
log.info("月榜热门视频更新完成");
|
||||
} catch (Exception e) {
|
||||
log.error("更新月榜热门视频失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新热门视频
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param periodType 周期类型(1:天,2:周,3:月)
|
||||
* @param limit 数量限制
|
||||
*/
|
||||
private void updateHotVideos(LocalDateTime startTime, int periodType, int limit) {
|
||||
// 设置行为类型和权重
|
||||
List<Integer> behaviorTypes = Arrays.asList(1, 2, 3, 4, 5, 6); // 浏览、点赞、收藏、评论、分享、完整观看
|
||||
Map<Integer, Float> weights = new HashMap<>();
|
||||
weights.put(1, viewWeight);
|
||||
weights.put(2, likeWeight);
|
||||
weights.put(3, favoriteWeight);
|
||||
weights.put(4, commentWeight);
|
||||
weights.put(5, shareWeight);
|
||||
weights.put(6, viewWeight * 2); // 完整观看权重翻倍
|
||||
|
||||
// 获取热门视频
|
||||
List<Map<String, Object>> hotVideos = userBehaviorMapper.selectHotVideos(
|
||||
startTime, behaviorTypes, weights, limit);
|
||||
|
||||
if (!hotVideos.isEmpty()) {
|
||||
// 先删除旧数据
|
||||
hotVideoMapper.deleteByPeriodType(periodType);
|
||||
|
||||
// 构建热门视频实体列表
|
||||
List<RecommendHotVideo> hotVideoEntities = new ArrayList<>();
|
||||
for (Map<String, Object> video : hotVideos) {
|
||||
Long videoId = Long.valueOf(video.get("videoId").toString());
|
||||
float score = Float.parseFloat(video.get("score").toString());
|
||||
|
||||
RecommendHotVideo hotVideo = new RecommendHotVideo();
|
||||
hotVideo.setVideoId(videoId);
|
||||
hotVideo.setHotScore(score);
|
||||
hotVideo.setPeriodType(periodType);
|
||||
hotVideo.setCreatedBy("system");
|
||||
hotVideo.setUpdatedBy("system");
|
||||
hotVideo.setCreateTime(LocalDateTime.now());
|
||||
hotVideo.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
hotVideoEntities.add(hotVideo);
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
if (!hotVideoEntities.isEmpty()) {
|
||||
hotVideoMapper.batchInsert(hotVideoEntities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package com.nuaa.video.recommend.task;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.recommend.entity.RecommendVideoSimilarity;
|
||||
import com.nuaa.video.recommend.feign.ContentFeignClient;
|
||||
import com.nuaa.video.recommend.mapper.RecommendVideoSimilarityMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.math3.linear.ArrayRealVector;
|
||||
import org.apache.commons.math3.linear.RealVector;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 视频相似度计算定时任务
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class VideoSimilarityCalculateTask {
|
||||
|
||||
private final ContentFeignClient contentFeignClient;
|
||||
private final RecommendVideoSimilarityMapper videoSimilarityMapper;
|
||||
|
||||
/**
|
||||
* 每天凌晨4点计算视频相似度
|
||||
*/
|
||||
@Scheduled(cron = "0 0 4 * * ?")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void calculateVideoSimilarity() {
|
||||
log.info("开始计算视频相似度");
|
||||
try {
|
||||
// 获取视频列表
|
||||
Result<Object> result = contentFeignClient.getVideoList(1, 1000, null, null,
|
||||
"create_time", "desc", null);
|
||||
|
||||
if (result.getCode() == 200 && result.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> pageData = (Map<String, Object>) result.getData();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> records = (List<Map<String, Object>>) pageData.get("records");
|
||||
|
||||
if (records != null && !records.isEmpty()) {
|
||||
// 获取所有标签
|
||||
Map<Long, Set<Long>> videoTagsMap = new HashMap<>();
|
||||
Map<Long, Long> videoCategoryMap = new HashMap<>();
|
||||
|
||||
// 提取每个视频的标签和分类
|
||||
for (Map<String, Object> video : records) {
|
||||
Long videoId = Long.valueOf(video.get("id").toString());
|
||||
Long categoryId = Long.valueOf(video.get("categoryId").toString());
|
||||
videoCategoryMap.put(videoId, categoryId);
|
||||
|
||||
// 获取视频标签
|
||||
Result<Object> tagResult = contentFeignClient.getVideoTags(videoId);
|
||||
if (tagResult.getCode() == 200 && tagResult.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> tags = (List<Map<String, Object>>) tagResult.getData();
|
||||
|
||||
Set<Long> tagIds = new HashSet<>();
|
||||
for (Map<String, Object> tag : tags) {
|
||||
tagIds.add(Long.valueOf(tag.get("id").toString()));
|
||||
}
|
||||
|
||||
videoTagsMap.put(videoId, tagIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算视频间的相似度
|
||||
List<RecommendVideoSimilarity> similarities = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Long videoId1 = Long.valueOf(records.get(i).get("id").toString());
|
||||
Set<Long> tags1 = videoTagsMap.getOrDefault(videoId1, new HashSet<>());
|
||||
Long category1 = videoCategoryMap.get(videoId1);
|
||||
|
||||
for (int j = i + 1; j < records.size(); j++) {
|
||||
Long videoId2 = Long.valueOf(records.get(j).get("id").toString());
|
||||
Set<Long> tags2 = videoTagsMap.getOrDefault(videoId2, new HashSet<>());
|
||||
Long category2 = videoCategoryMap.get(videoId2);
|
||||
|
||||
// 计算标签相似度
|
||||
float tagSimilarity = calculateTagSimilarity(tags1, tags2);
|
||||
|
||||
// 计算分类相似度
|
||||
float categorySimilarity = category1.equals(category2) ? 1.0f : 0.0f;
|
||||
|
||||
// 综合相似度 (标签权重0.7,分类权重0.3)
|
||||
float similarity = tagSimilarity * 0.7f + categorySimilarity * 0.3f;
|
||||
|
||||
// 只保存相似度大于0.3的记录
|
||||
if (similarity > 0.3f) {
|
||||
// 创建相似度记录
|
||||
RecommendVideoSimilarity sim1 = new RecommendVideoSimilarity();
|
||||
sim1.setVideoId(videoId1);
|
||||
sim1.setSimilarVideoId(videoId2);
|
||||
sim1.setSimilarity(similarity);
|
||||
sim1.setCreatedBy("system");
|
||||
sim1.setUpdatedBy("system");
|
||||
sim1.setCreateTime(LocalDateTime.now());
|
||||
sim1.setUpdateTime(LocalDateTime.now());
|
||||
similarities.add(sim1);
|
||||
|
||||
// 相似度是双向的
|
||||
RecommendVideoSimilarity sim2 = new RecommendVideoSimilarity();
|
||||
sim2.setVideoId(videoId2);
|
||||
sim2.setSimilarVideoId(videoId1);
|
||||
sim2.setSimilarity(similarity);
|
||||
sim2.setCreatedBy("system");
|
||||
sim2.setUpdatedBy("system");
|
||||
sim2.setCreateTime(LocalDateTime.now());
|
||||
sim2.setUpdateTime(LocalDateTime.now());
|
||||
similarities.add(sim2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新视频相似度
|
||||
if (!similarities.isEmpty()) {
|
||||
// 每批处理100条
|
||||
int batchSize = 100;
|
||||
for (int i = 0; i < similarities.size(); i += batchSize) {
|
||||
int endIndex = Math.min(i + batchSize, similarities.size());
|
||||
List<RecommendVideoSimilarity> batch = similarities.subList(i, endIndex);
|
||||
videoSimilarityMapper.batchInsertOrUpdate(batch);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("视频相似度计算完成,共处理{}个视频,生成{}条相似度记录", records.size(), similarities.size());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("计算视频相似度失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算标签相似度 (Jaccard相似度)
|
||||
*/
|
||||
private float calculateTagSimilarity(Set<Long> tags1, Set<Long> tags2) {
|
||||
if (tags1.isEmpty() && tags2.isEmpty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// 计算交集大小
|
||||
Set<Long> intersection = new HashSet<>(tags1);
|
||||
intersection.retainAll(tags2);
|
||||
|
||||
// 计算并集大小
|
||||
Set<Long> union = new HashSet<>(tags1);
|
||||
union.addAll(tags2);
|
||||
|
||||
// Jaccard相似度 = 交集大小 / 并集大小
|
||||
return (float) intersection.size() / union.size();
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.nuaa.video.recommend.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 分类兴趣VO
|
||||
*/
|
||||
@Data
|
||||
public class CategoryInterestVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 分类ID
|
||||
*/
|
||||
private Long categoryId;
|
||||
|
||||
/**
|
||||
* 分类名称
|
||||
*/
|
||||
private String categoryName;
|
||||
|
||||
/**
|
||||
* 偏好权重
|
||||
*/
|
||||
private Float weight;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.nuaa.video.recommend.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 标签兴趣VO
|
||||
*/
|
||||
@Data
|
||||
public class TagInterestVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 标签ID
|
||||
*/
|
||||
private Long tagId;
|
||||
|
||||
/**
|
||||
* 标签名称
|
||||
*/
|
||||
private String tagName;
|
||||
|
||||
/**
|
||||
* 兴趣权重
|
||||
*/
|
||||
private Float weight;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.nuaa.video.recommend.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户兴趣VO
|
||||
*/
|
||||
@Data
|
||||
public class UserInterestVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户标签列表
|
||||
*/
|
||||
private List<TagInterestVO> interests;
|
||||
|
||||
/**
|
||||
* 分类偏好列表
|
||||
*/
|
||||
private List<CategoryInterestVO> categoryPreferences;
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.nuaa.video.recommend.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 视频推荐VO
|
||||
*/
|
||||
@Data
|
||||
public class VideoRecommendVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 视频描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 封面URL
|
||||
*/
|
||||
private String coverUrl;
|
||||
|
||||
/**
|
||||
* 视频时长(秒)
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String userNickname;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 分类ID
|
||||
*/
|
||||
private Long categoryId;
|
||||
|
||||
/**
|
||||
* 分类名称
|
||||
*/
|
||||
private String categoryName;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 评论数
|
||||
*/
|
||||
private Integer commentCount;
|
||||
|
||||
/**
|
||||
* 播放数
|
||||
*/
|
||||
private Long viewCount;
|
||||
|
||||
/**
|
||||
* 标签名称列表
|
||||
*/
|
||||
private List<String> tagNames;
|
||||
|
||||
/**
|
||||
* 推荐原因
|
||||
*/
|
||||
private String recommendReason;
|
||||
|
||||
/**
|
||||
* 发布时间
|
||||
*/
|
||||
private LocalDateTime publishTime;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
server:
|
||||
port: 9006
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: video-recommend
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
sentinel:
|
||||
transport:
|
||||
dashboard: ${SENTINEL_HOST:sentinel}:${SENTINEL_PORT:8080}
|
||||
port: 8726
|
||||
eager: true
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/video_recommend?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: redis_EsJ8FX
|
||||
database: 5
|
||||
timeout: 10000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 200
|
||||
max-wait: -1
|
||||
max-idle: 10
|
||||
min-idle: 0
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
type-aliases-package: com.nuaa.video.recommend.entity
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
@@ -0,0 +1,38 @@
|
||||
server:
|
||||
port: 9005
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: video-recommend
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:common.yaml?refreshEnabled=true
|
||||
- optional:nacos:common-redis-${spring.profiles.active}.yaml?refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}.yaml?refreshEnabled=true
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
ip: ${SPRING_CLOUD_NACOS_DISCOVERY_IP:}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
namespace: ${spring.cloud.nacos.discovery.namespace}
|
||||
group: ${spring.cloud.nacos.discovery.group}
|
||||
file-extension: yaml
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,23 @@
|
||||
spring:
|
||||
application:
|
||||
name: video-recommend
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
ephemeral: true
|
||||
ip: ${RECOMMEND_IP:}
|
||||
port: ${server.port:9006}
|
||||
fail-fast: true
|
||||
register-enabled: true
|
||||
config:
|
||||
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
file-extension: yaml
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.recommend.mapper.RecommendHotVideoMapper">
|
||||
|
||||
<!-- 获取热门视频ID列表 -->
|
||||
<select id="selectHotVideoIds" resultType="java.lang.Long">
|
||||
SELECT video_id
|
||||
FROM recommend_hot_video
|
||||
WHERE period_type = #{periodType}
|
||||
ORDER BY hot_score DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 批量插入热门视频 -->
|
||||
<insert id="batchInsert">
|
||||
INSERT INTO recommend_hot_video (
|
||||
video_id, hot_score, period_type,
|
||||
created_by, updated_by, create_time, update_time
|
||||
)
|
||||
VALUES
|
||||
<foreach collection="hotVideos" item="item" separator=",">
|
||||
(
|
||||
#{item.videoId}, #{item.hotScore}, #{item.periodType},
|
||||
#{item.createdBy}, #{item.updatedBy}, NOW(), NOW()
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 删除指定周期的热门视频 -->
|
||||
<delete id="deleteByPeriodType">
|
||||
DELETE FROM recommend_hot_video
|
||||
WHERE period_type = #{periodType}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.recommend.mapper.RecommendUserBehaviorMapper">
|
||||
|
||||
<!-- 获取用户行为记录的视频ID列表 -->
|
||||
<select id="selectUserBehaviorVideoIds" resultType="java.lang.Long">
|
||||
SELECT DISTINCT video_id
|
||||
FROM recommend_user_behavior
|
||||
WHERE user_id = #{userId}
|
||||
<if test="behaviorTypes != null and behaviorTypes.size > 0">
|
||||
AND behavior_type IN
|
||||
<foreach collection="behaviorTypes" item="type" open="(" separator="," close=")">
|
||||
#{type}
|
||||
</foreach>
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 统计用户分类行为数量 -->
|
||||
<select id="countUserCategoryBehaviors" resultType="java.util.Map">
|
||||
SELECT c.category_id as categoryId, COUNT(1) as count
|
||||
FROM recommend_user_behavior b
|
||||
JOIN video_content.content_video v ON b.video_id = v.id
|
||||
JOIN video_content.content_category c ON v.category_id = c.id
|
||||
WHERE b.user_id = #{userId}
|
||||
<if test="startTime != null">
|
||||
AND b.create_time >= #{startTime}
|
||||
</if>
|
||||
GROUP BY c.category_id
|
||||
</select>
|
||||
|
||||
<!-- 统计用户标签行为数量 -->
|
||||
<select id="countUserTagBehaviors" resultType="java.util.Map">
|
||||
SELECT vt.tag_id as tagId, COUNT(1) as count
|
||||
FROM recommend_user_behavior b
|
||||
JOIN video_content.content_video_tag vt ON b.video_id = vt.video_id
|
||||
WHERE b.user_id = #{userId}
|
||||
<if test="startTime != null">
|
||||
AND b.create_time >= #{startTime}
|
||||
</if>
|
||||
GROUP BY vt.tag_id
|
||||
</select>
|
||||
|
||||
<!-- 获取热门视频 -->
|
||||
<select id="selectHotVideos" resultType="java.util.Map">
|
||||
SELECT
|
||||
video_id as videoId,
|
||||
SUM(
|
||||
CASE
|
||||
<foreach collection="weights.entrySet()" item="entry">
|
||||
WHEN behavior_type = #{entry.key} THEN behavior_weight * #{entry.value}
|
||||
</foreach>
|
||||
ELSE behavior_weight
|
||||
END
|
||||
) as score
|
||||
FROM recommend_user_behavior
|
||||
WHERE
|
||||
<if test="startTime != null">
|
||||
create_time >= #{startTime}
|
||||
</if>
|
||||
<if test="behaviorTypes != null and behaviorTypes.size > 0">
|
||||
AND behavior_type IN
|
||||
<foreach collection="behaviorTypes" item="type" open="(" separator="," close=")">
|
||||
#{type}
|
||||
</foreach>
|
||||
</if>
|
||||
GROUP BY video_id
|
||||
ORDER BY score DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.recommend.mapper.RecommendUserInterestMapper">
|
||||
|
||||
<!-- 获取用户标签权重前N的标签ID -->
|
||||
<select id="selectTopTagIds" resultType="java.lang.Long">
|
||||
SELECT tag_id
|
||||
FROM recommend_user_interest
|
||||
WHERE user_id = #{userId}
|
||||
ORDER BY weight DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 批量更新用户标签权重 -->
|
||||
<update id="batchUpdateWeights">
|
||||
<foreach collection="tagWeights.entrySet()" item="entry" separator=";">
|
||||
INSERT INTO recommend_user_interest (user_id, tag_id, weight, created_by, updated_by, create_time, update_time)
|
||||
VALUES (#{userId}, #{entry.key}, #{entry.value}, 'system', 'system', NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
weight = VALUES(weight),
|
||||
updated_by = VALUES(updated_by),
|
||||
update_time = VALUES(update_time)
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<!-- 获取用户所有标签权重 -->
|
||||
<select id="selectUserTagWeights" resultType="java.util.Map">
|
||||
SELECT tag_id, weight
|
||||
FROM recommend_user_interest
|
||||
WHERE user_id = #{userId}
|
||||
</select>
|
||||
</mapper>
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.recommend.mapper.RecommendUserRecommendationMapper">
|
||||
|
||||
<!-- 获取用户推荐视频ID列表 -->
|
||||
<select id="selectUserRecommendVideoIds" resultType="java.lang.Long">
|
||||
SELECT video_id
|
||||
FROM recommend_user_recommendation
|
||||
WHERE user_id = #{userId}
|
||||
<if test="recommendType != null">
|
||||
AND recommend_type = #{recommendType}
|
||||
</if>
|
||||
ORDER BY score DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 获取用户所有推荐视频ID -->
|
||||
<select id="selectAllUserRecommendVideoIds" resultType="java.lang.Long">
|
||||
SELECT video_id
|
||||
FROM recommend_user_recommendation
|
||||
WHERE user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<!-- 批量插入用户推荐记录 -->
|
||||
<insert id="batchInsert">
|
||||
INSERT INTO recommend_user_recommendation (
|
||||
user_id, video_id, recommend_type, score,
|
||||
created_by, updated_by, create_time, update_time
|
||||
)
|
||||
VALUES
|
||||
<foreach collection="recommendations" item="item" separator=",">
|
||||
(
|
||||
#{item.userId}, #{item.videoId}, #{item.recommendType}, #{item.score},
|
||||
#{item.createdBy}, #{item.updatedBy}, NOW(), NOW()
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 删除用户指定类型的推荐记录 -->
|
||||
<delete id="deleteUserRecommendations">
|
||||
DELETE FROM recommend_user_recommendation
|
||||
WHERE user_id = #{userId}
|
||||
<if test="recommendType != null">
|
||||
AND recommend_type = #{recommendType}
|
||||
</if>
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.recommend.mapper.RecommendVideoSimilarityMapper">
|
||||
|
||||
<!-- 获取相似视频列表 -->
|
||||
<select id="selectSimilarVideoIds" resultType="java.lang.Long">
|
||||
SELECT similar_video_id
|
||||
FROM recommend_video_similarity
|
||||
WHERE video_id = #{videoId}
|
||||
AND similarity >= #{threshold}
|
||||
ORDER BY similarity DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 批量插入或更新视频相似度 -->
|
||||
<insert id="batchInsertOrUpdate">
|
||||
<foreach collection="similarities" item="item" separator=";">
|
||||
INSERT INTO recommend_video_similarity (
|
||||
video_id, similar_video_id, similarity,
|
||||
created_by, updated_by, create_time, update_time
|
||||
)
|
||||
VALUES (
|
||||
#{item.videoId}, #{item.similarVideoId}, #{item.similarity},
|
||||
#{item.createdBy}, #{item.updatedBy}, NOW(), NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
similarity = VALUES(similarity),
|
||||
updated_by = VALUES(updated_by),
|
||||
update_time = VALUES(update_time)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 获取视频相似度TOP N -->
|
||||
<select id="selectTopSimilarVideos" resultType="java.util.Map">
|
||||
SELECT similar_video_id as videoId, similarity as score
|
||||
FROM recommend_video_similarity
|
||||
WHERE video_id = #{videoId}
|
||||
ORDER BY similarity DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user