fix(backend): Restore service flow and admin management integration

This commit is contained in:
2026-04-18 18:20:58 +08:00
parent 92a139e470
commit 3adb088eca
83 changed files with 1696 additions and 703 deletions
@@ -28,7 +28,9 @@ public class SecurityConfig {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/recommend/home",
"/recommend/hot/**",
"/recommend/similar/**",
"/recommend/tag/**",
"/actuator/**",
"/error"
@@ -39,4 +41,4 @@ public class SecurityConfig {
return http.build();
}
}
}
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT认证过滤器
@@ -29,7 +30,7 @@ import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
@Value("${jwt.secret}")
private String secret;
private static final String TOKEN_PREFIX = "Bearer ";
@@ -55,7 +56,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String username = claims.get("username", String.class);
// 设置用户权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<String> authorityClaims = claims.get("authorities", List.class);
List<SimpleGrantedAuthority> authorities = authorityClaims == null
? new ArrayList<>()
: authorityClaims.stream()
.filter(StringUtils::hasText)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toCollection(ArrayList::new));
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
@@ -70,6 +77,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 在请求属性中存储用户ID
request.setAttribute("userId", userId);
request.setAttribute("username", username);
if (!authorities.isEmpty()) {
request.setAttribute("adminId", userId);
}
log.debug("认证成功: 用户ID={}, 用户名={}", userId, username);
} catch (Exception e) {
@@ -99,4 +109,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
}
}
@@ -22,6 +22,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.stream.Collectors;
@@ -139,7 +141,7 @@ public class RecommendServiceImpl implements RecommendService {
Long categoryId = null;
if (videoData.containsKey("categoryId")) {
categoryId = Long.valueOf(videoData.get("categoryId").toString());
categoryId = toLong(videoData.get("categoryId"));
}
if (categoryId != null) {
@@ -154,8 +156,8 @@ public class RecommendServiceImpl implements RecommendService {
if (records != null) {
for (Map<String, Object> video : records) {
Long id = Long.valueOf(video.get("id").toString());
if (!id.equals(videoId)) { // 过滤掉当前视频
Long id = toLong(video.get("id"));
if (id != null && !id.equals(videoId)) { // 过滤掉当前视频
VideoRecommendVO vo = mapToRecommendVO(video);
vo.setRecommendReason("相关推荐");
recommendList.add(vo);
@@ -168,7 +170,10 @@ public class RecommendServiceImpl implements RecommendService {
} else {
// 有相似视频记录,获取视频详情
for (Map<String, Object> similar : similarVideos) {
Long similarVideoId = Long.valueOf(similar.get("videoId").toString());
Long similarVideoId = toLong(similar.get("videoId"));
if (similarVideoId == null) {
continue;
}
Result<Object> result = contentFeignClient.getVideoInfo(similarVideoId);
if (result.getCode() == 200 && result.getData() != null) {
@@ -317,20 +322,28 @@ public class RecommendServiceImpl implements RecommendService {
if (!tagBehaviors.isEmpty()) {
// 计算标签总权重
double totalWeight = tagBehaviors.stream()
.mapToDouble(tag -> Double.parseDouble(tag.get("count").toString()))
.mapToDouble(tag -> toDouble(tag.get("count")))
.sum();
if (totalWeight <= 0) {
return Result.success();
}
// 计算标签权重
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());
Long tagId = toLong(tag.get("tagId"));
double count = toDouble(tag.get("count"));
if (tagId == null || count <= 0) {
continue;
}
float weight = (float) (count / totalWeight);
tagWeights.put(tagId, weight);
}
// 更新用户标签兴趣
userInterestMapper.batchUpdateWeights(userId, tagWeights);
if (!tagWeights.isEmpty()) {
userInterestMapper.batchUpdateWeights(userId, tagWeights);
}
}
// 2. 统计分类行为数量(如果需要可以添加分类兴趣统计)
@@ -349,19 +362,19 @@ public class RecommendServiceImpl implements RecommendService {
VideoRecommendVO vo = new VideoRecommendVO();
// 设置基本信息
if (videoData.containsKey("id")) vo.setId(Long.valueOf(videoData.get("id").toString()));
if (videoData.containsKey("id")) vo.setId(toLong(videoData.get("id")));
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("duration")) vo.setDuration(toInt(videoData.get("duration")));
if (videoData.containsKey("userId")) vo.setUserId(toLong(videoData.get("userId")));
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("categoryId")) vo.setCategoryId(toLong(videoData.get("categoryId")));
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("likeCount")) vo.setLikeCount(toInt(videoData.get("likeCount")));
if (videoData.containsKey("commentCount")) vo.setCommentCount(toInt(videoData.get("commentCount")));
if (videoData.containsKey("viewCount")) vo.setViewCount(toLong(videoData.get("viewCount")));
// 设置标签
if (videoData.containsKey("tagNames")) {
@@ -373,9 +386,74 @@ public class RecommendServiceImpl implements RecommendService {
// 设置发布时间
if (videoData.containsKey("publishTime")) {
String publishTimeStr = videoData.get("publishTime").toString();
vo.setPublishTime(LocalDateTime.parse(publishTimeStr));
vo.setPublishTime(parseDateTime(publishTimeStr));
}
return vo;
}
}
private LocalDateTime parseDateTime(String value) {
if (value == null || value.isBlank()) {
return null;
}
List<DateTimeFormatter> formatters = List.of(
DateTimeFormatter.ISO_DATE_TIME,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.S")
);
for (DateTimeFormatter formatter : formatters) {
try {
return LocalDateTime.parse(value, formatter);
} catch (DateTimeParseException ignored) {
}
}
log.warn("无法解析发布时间: {}", value);
return null;
}
private Long toLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
String text = value.toString().trim();
if (text.isEmpty()) {
return null;
}
try {
return Long.parseLong(text);
} catch (NumberFormatException e) {
log.warn("无法解析Long值: {}", value);
return null;
}
}
private Integer toInt(Object value) {
Long longValue = toLong(value);
return longValue == null ? null : longValue.intValue();
}
private double toDouble(Object value) {
if (value == null) {
return 0D;
}
if (value instanceof Number number) {
return number.doubleValue();
}
String text = value.toString().trim();
if (text.isEmpty()) {
return 0D;
}
try {
return Double.parseDouble(text);
} catch (NumberFormatException e) {
log.warn("无法解析Double值: {}", value);
return 0D;
}
}
}
@@ -69,8 +69,11 @@ public class ContentBasedRecommendStrategy implements RecommendStrategy {
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());
Long similarVideoId = toLong(video.get("videoId"));
Double similarity = toDouble(video.get("score"));
if (similarVideoId == null || similarity == null) {
continue;
}
// 过滤掉用户已经看过的视频
if (!recentVideoIds.contains(similarVideoId)) {
@@ -109,8 +112,11 @@ public class ContentBasedRecommendStrategy implements RecommendStrategy {
// 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()));
Long tagId = toLong(weight.get("tag_id"));
Float score = toFloat(weight.get("weight"));
if (tagId != null && score != null && score > 0F) {
tagWeightMap.put(tagId, score);
}
}
// 3. 获取带权重的推荐标签ID
@@ -143,7 +149,10 @@ public class ContentBasedRecommendStrategy implements RecommendStrategy {
if (records != null) {
for (Map<String, Object> video : records) {
Long videoId = Long.valueOf(video.get("id").toString());
Long videoId = toLong(video.get("id"));
if (videoId == null) {
continue;
}
// 过滤掉已经推荐的视频
if (!recommendedVideoIds.contains(videoId)) {
@@ -191,19 +200,19 @@ public class ContentBasedRecommendStrategy implements RecommendStrategy {
VideoRecommendVO vo = new VideoRecommendVO();
// 设置基本信息
if (videoData.containsKey("id")) vo.setId(Long.valueOf(videoData.get("id").toString()));
if (videoData.containsKey("id")) vo.setId(toLong(videoData.get("id")));
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("duration")) vo.setDuration(toInt(videoData.get("duration")));
if (videoData.containsKey("userId")) vo.setUserId(toLong(videoData.get("userId")));
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("categoryId")) vo.setCategoryId(toLong(videoData.get("categoryId")));
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("likeCount")) vo.setLikeCount(toInt(videoData.get("likeCount")));
if (videoData.containsKey("commentCount")) vo.setCommentCount(toInt(videoData.get("commentCount")));
if (videoData.containsKey("viewCount")) vo.setViewCount(toLong(videoData.get("viewCount")));
// 设置标签
if (videoData.containsKey("tagNames")) {
@@ -214,4 +223,44 @@ public class ContentBasedRecommendStrategy implements RecommendStrategy {
return vo;
}
}
private Long toLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
log.warn("推荐数据Long解析失败: {}", value);
return null;
}
}
private Integer toInt(Object value) {
Long longValue = toLong(value);
return longValue == null ? null : longValue.intValue();
}
private Double toDouble(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (NumberFormatException e) {
log.warn("推荐数据Double解析失败: {}", value);
return null;
}
}
private Float toFloat(Object value) {
Double doubleValue = toDouble(value);
return doubleValue == null ? null : doubleValue.floatValue();
}
}
@@ -50,18 +50,26 @@ public class HotRecommendStrategy implements RecommendStrategy {
}
// 获取每日热门视频
List<Long> hotVideoIds = hotVideoMapper.selectHotVideoIds(1, size);
List<Long> hotVideoIds = new ArrayList<>(hotVideoMapper.selectHotVideoIds(1, size));
// 如果每日热门不足,则获取每周热门补充
if (hotVideoIds.size() < size) {
List<Long> weeklyHotVideoIds = hotVideoMapper.selectHotVideoIds(2, size - hotVideoIds.size());
hotVideoIds.addAll(weeklyHotVideoIds);
for (Long videoId : weeklyHotVideoIds) {
if (!hotVideoIds.contains(videoId)) {
hotVideoIds.add(videoId);
}
}
}
// 如果仍然不足,则获取每月热门补充
if (hotVideoIds.size() < size) {
List<Long> monthlyHotVideoIds = hotVideoMapper.selectHotVideoIds(3, size - hotVideoIds.size());
hotVideoIds.addAll(monthlyHotVideoIds);
for (Long videoId : monthlyHotVideoIds) {
if (!hotVideoIds.contains(videoId)) {
hotVideoIds.add(videoId);
}
}
}
// 调用内容服务获取视频详情
@@ -97,19 +105,19 @@ public class HotRecommendStrategy implements RecommendStrategy {
VideoRecommendVO vo = new VideoRecommendVO();
// 设置基本信息
if (videoData.containsKey("id")) vo.setId(Long.valueOf(videoData.get("id").toString()));
if (videoData.containsKey("id")) vo.setId(toLong(videoData.get("id")));
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("duration")) vo.setDuration(toInt(videoData.get("duration")));
if (videoData.containsKey("userId")) vo.setUserId(toLong(videoData.get("userId")));
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("categoryId")) vo.setCategoryId(toLong(videoData.get("categoryId")));
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("likeCount")) vo.setLikeCount(toInt(videoData.get("likeCount")));
if (videoData.containsKey("commentCount")) vo.setCommentCount(toInt(videoData.get("commentCount")));
if (videoData.containsKey("viewCount")) vo.setViewCount(toLong(videoData.get("viewCount")));
// 设置标签
if (videoData.containsKey("tagNames")) {
@@ -120,4 +128,24 @@ public class HotRecommendStrategy implements RecommendStrategy {
return vo;
}
}
private Long toLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
log.warn("热门推荐Long解析失败: {}", value);
return null;
}
}
private Integer toInt(Object value) {
Long longValue = toLong(value);
return longValue == null ? null : longValue.intValue();
}
}
@@ -119,8 +119,11 @@ public class HotVideoUpdateTask {
// 构建热门视频实体列表
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());
Long videoId = toLong(video.get("videoId"));
Float score = toFloat(video.get("score"));
if (videoId == null || score == null) {
continue;
}
RecommendHotVideo hotVideo = new RecommendHotVideo();
hotVideo.setVideoId(videoId);
@@ -140,4 +143,34 @@ public class HotVideoUpdateTask {
}
}
}
}
private Long toLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
log.warn("热门任务Long解析失败: {}", value);
return null;
}
}
private Float toFloat(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.floatValue();
}
try {
return Float.parseFloat(value.toString());
} catch (NumberFormatException e) {
log.warn("热门任务Float解析失败: {}", value);
return null;
}
}
}
@@ -1,5 +1,5 @@
server:
port: 9006
port: 9005
spring:
application:
@@ -18,14 +18,14 @@ spring:
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
password: ${MYSQL_ROOT_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: ${REDIS_HOST:redis}
port: ${REDIS_PORT:6379}
password: redis_EsJ8FX
database: 5
password: ${REDIS_PASSWORD}
database: 4
timeout: 10000
lettuce:
pool:
@@ -40,10 +40,15 @@ mybatis-plus:
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
logic-not-delete-value: 0
jwt:
secret: ${USER_JWT_SECRET}
expiration: ${JWT_EXPIRATION}
token-header: Authorization
token-prefix: Bearer
@@ -13,9 +13,9 @@
<!-- 批量更新用户标签权重 -->
<update id="batchUpdateWeights">
<foreach collection="tagWeights.entrySet()" item="entry" separator=";">
<foreach collection="tagWeights" index="tagId" item="weight" 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())
VALUES (#{userId}, #{tagId}, #{weight}, 'system', 'system', NOW(), NOW())
ON DUPLICATE KEY UPDATE
weight = VALUES(weight),
updated_by = VALUES(updated_by),
@@ -29,4 +29,4 @@
FROM recommend_user_interest
WHERE user_id = #{userId}
</select>
</mapper>
</mapper>