feat(content): Implement content module foundation

This commit is contained in:
2025-04-08 01:16:42 +08:00
parent 730a23dee3
commit 6b5bd6daaf
90 changed files with 6284 additions and 21 deletions
@@ -1,11 +1,20 @@
package com.nuaa.video.content;
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.content.mapper")
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class, args);
@@ -0,0 +1,68 @@
package com.nuaa.video.content.config;
import lombok.Data;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFprobe;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* FFmpeg配置类
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "ffmpeg")
public class FFmpegConfig {
/**
* binary配置
*/
private Binary binary = new Binary();
/**
* probe配置
*/
private Probe probe = new Probe();
/**
* 临时文件配置
*/
private Temp temp = new Temp();
@Data
public static class Binary {
/**
* FFmpeg可执行文件路径
*/
private String path = "/usr/bin/ffmpeg";
}
@Data
public static class Probe {
/**
* FFprobe可执行文件路径
*/
private String path = "/usr/bin/ffprobe";
}
@Data
public static class Temp {
/**
* 临时文件目录
*/
private String path = "/tmp/ffmpeg";
}
@Bean
public FFmpeg ffmpeg() throws IOException {
return new FFmpeg(binary.getPath());
}
@Bean
public FFprobe ffprobe() throws IOException {
return new FFprobe(probe.getPath());
}
}
@@ -0,0 +1,52 @@
package com.nuaa.video.content.config;
import com.nuaa.video.common.utils.MinioUtils;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinIO配置类
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/**
* 服务地址
*/
private String endpoint;
/**
* 访问密钥
*/
private String accessKey;
/**
* 密钥
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
@Bean
public MinioUtils minioUtils(MinioClient minioClient) {
MinioUtils minioUtils = new MinioUtils(minioClient);
// 如果MinioUtils有setBucketName方法或需要其他配置,在这里设置
return minioUtils;
}
}
@@ -0,0 +1,29 @@
package com.nuaa.video.content.config;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RocketConfig {
@Value("${spring.rocketmq.producer.group}")
private String producerGroup;
@Value("${spring.rocketmq.name-server}")
private String nameServer;
@Bean(name = "rocketMQTemplate")
public RocketMQTemplate rocketMQTemplate() {
RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
DefaultMQProducer defaultMQProducer = new DefaultMQProducer(producerGroup);
defaultMQProducer.setProducerGroup(producerGroup);
defaultMQProducer.setNamesrvAddr(nameServer);
rocketMQTemplate.setProducer(defaultMQProducer);
return rocketMQTemplate;
}
}
@@ -0,0 +1,43 @@
package com.nuaa.video.content.config;
import com.nuaa.video.content.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(
"/content/video/public/**",
"/content/category/list",
"/content/tag/list",
"/actuator/**",
"/error"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@@ -0,0 +1,80 @@
package com.nuaa.video.content.controller;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentCategory;
import com.nuaa.video.content.service.CategoryService;
import com.nuaa.video.content.vo.CategoryVO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 分类控制器
*/
@Slf4j
@RestController
@RequestMapping("/content/category")
@RequiredArgsConstructor
public class CategoryController {
private final CategoryService categoryService;
/**
* 获取分类树
*/
@GetMapping("/tree")
public Result<List<CategoryVO>> getCategoryTree() {
return categoryService.getCategoryTree();
}
/**
* 获取分类列表
*/
@GetMapping("/list")
public Result<List<CategoryVO>> getCategoryList() {
return categoryService.getCategoryList();
}
/**
* 添加分类
*/
@PostMapping
public Result<Void> addCategory(@RequestBody ContentCategory category, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
category.setCreatedBy(userId.toString());
category.setUpdatedBy(userId.toString());
return categoryService.addCategory(category);
}
/**
* 更新分类
*/
@PutMapping
public Result<Void> updateCategory(@RequestBody ContentCategory category, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
category.setUpdatedBy(userId.toString());
return categoryService.updateCategory(category);
}
/**
* 删除分类
*/
@DeleteMapping("/{id}")
public Result<Void> deleteCategory(@PathVariable Long id) {
return categoryService.deleteCategory(id);
}
/**
* 从请求中获取用户ID
*/
private Long getUserIdFromRequest(HttpServletRequest request) {
Object userIdObj = request.getAttribute("userId");
if (userIdObj instanceof Long) {
return (Long) userIdObj;
}
return null;
}
}
@@ -0,0 +1,67 @@
package com.nuaa.video.content.controller;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.dto.DanmakuDTO;
import com.nuaa.video.content.service.DanmakuService;
import com.nuaa.video.content.vo.DanmakuVO;
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("/content/danmaku")
@RequiredArgsConstructor
public class DanmakuController {
private final DanmakuService danmakuService;
/**
* 发送弹幕
*/
@PostMapping
public Result<Long> sendDanmaku(@RequestBody @Valid DanmakuDTO danmakuDTO, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return danmakuService.sendDanmaku(danmakuDTO, userId);
}
/**
* 获取视频指定时间段的弹幕
*/
@GetMapping("/time")
public Result<List<DanmakuVO>> getVideoTimeDanmaku(
@RequestParam Long videoId,
@RequestParam Float startTime,
@RequestParam Float endTime) {
return danmakuService.getVideoTimeDanmaku(videoId, startTime, endTime);
}
/**
* 获取视频所有弹幕
*/
@GetMapping("/all/{videoId}")
public Result<List<DanmakuVO>> getVideoAllDanmaku(@PathVariable Long videoId) {
return danmakuService.getVideoAllDanmaku(videoId);
}
/**
* 从请求中获取用户ID
*/
private Long getUserIdFromRequest(HttpServletRequest request) {
Object userIdObj = request.getAttribute("userId");
if (userIdObj instanceof Long) {
return (Long) userIdObj;
}
return null;
}
}
@@ -0,0 +1,107 @@
package com.nuaa.video.content.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentUserFavorite;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.service.FavoriteService;
import com.nuaa.video.content.vo.FavoriteVO;
import com.nuaa.video.content.vo.VideoListVO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 收藏夹控制器
*/
@Slf4j
@RestController
@RequestMapping("/content/favorite")
@RequiredArgsConstructor
public class FavoriteController {
private final FavoriteService favoriteService;
/**
* 获取用户收藏夹列表
*/
@GetMapping("/list")
public Result<List<FavoriteVO>> getUserFavorites(HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return favoriteService.getUserFavorites(userId);
}
/**
* 创建收藏夹
*/
@PostMapping
public Result<Long> createFavorite(@RequestBody ContentUserFavorite favorite, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
favorite.setUserId(userId);
favorite.setCreatedBy(userId.toString());
favorite.setUpdatedBy(userId.toString());
return favoriteService.createFavorite(favorite, userId);
}
/**
* 更新收藏夹
*/
@PutMapping
public Result<Void> updateFavorite(@RequestBody ContentUserFavorite favorite, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
favorite.setUpdatedBy(userId.toString());
return favoriteService.updateFavorite(favorite, userId);
}
/**
* 删除收藏夹
*/
@DeleteMapping("/{id}")
public Result<Void> deleteFavorite(@PathVariable Long id, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return favoriteService.deleteFavorite(id, userId);
}
/**
* 获取收藏夹中的视频
*/
@GetMapping("/{favoriteId}/videos")
public Result<Page<VideoListVO>> getFavoriteVideos(
@PathVariable Long favoriteId,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
Page<ContentVideo> page = new Page<>(pageNum, pageSize);
return favoriteService.getFavoriteVideos(favoriteId, page, userId);
}
/**
* 从请求中获取用户ID
*/
private Long getUserIdFromRequest(HttpServletRequest request) {
Object userIdObj = request.getAttribute("userId");
if (userIdObj instanceof Long) {
return (Long) userIdObj;
}
return null;
}
}
@@ -0,0 +1,93 @@
package com.nuaa.video.content.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentTag;
import com.nuaa.video.content.service.TagService;
import com.nuaa.video.content.vo.TagVO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 标签控制器
*/
@Slf4j
@RestController
@RequestMapping("/content/tag")
@RequiredArgsConstructor
public class TagController {
private final TagService tagService;
/**
* 获取标签列表
*/
@GetMapping("/list")
public Result<List<TagVO>> getTagList() {
return tagService.getTagList();
}
/**
* 分页获取标签列表
*/
@GetMapping("/page")
public Result<Page<TagVO>> getTagPage(
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "keyword", required = false) String keyword) {
Page<ContentTag> page = new Page<>(pageNum, pageSize);
return tagService.getTagPage(page, keyword);
}
/**
* 添加标签
*/
@PostMapping
public Result<Void> addTag(@RequestBody ContentTag tag, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
tag.setCreatedBy(userId.toString());
tag.setUpdatedBy(userId.toString());
return tagService.addTag(tag);
}
/**
* 更新标签
*/
@PutMapping
public Result<Void> updateTag(@RequestBody ContentTag tag, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
tag.setUpdatedBy(userId.toString());
return tagService.updateTag(tag);
}
/**
* 删除标签
*/
@DeleteMapping("/{id}")
public Result<Void> deleteTag(@PathVariable Long id) {
return tagService.deleteTag(id);
}
/**
* 获取视频标签
*/
@GetMapping("/video/{videoId}")
public Result<List<TagVO>> getVideoTags(@PathVariable Long videoId) {
return tagService.getVideoTags(videoId);
}
/**
* 从请求中获取用户ID
*/
private Long getUserIdFromRequest(HttpServletRequest request) {
Object userIdObj = request.getAttribute("userId");
if (userIdObj instanceof Long) {
return (Long) userIdObj;
}
return null;
}
}
@@ -0,0 +1,230 @@
package com.nuaa.video.content.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.dto.*;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.service.VideoService;
import com.nuaa.video.content.vo.UploadResultVO;
import com.nuaa.video.content.vo.VideoInfoVO;
import com.nuaa.video.content.vo.VideoListVO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 视频控制器
*/
@Slf4j
@RestController
@RequestMapping("/content/video")
@RequiredArgsConstructor
public class VideoController {
private final VideoService videoService;
/**
* 上传视频
*/
@PostMapping("/upload")
public Result<UploadResultVO> uploadVideo(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.uploadVideo(file, userId);
}
/**
* 创建视频
*/
@PostMapping
public Result<Long> createVideo(@RequestBody @Valid VideoUploadDTO uploadDTO, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.createVideo(uploadDTO, userId);
}
/**
* 获取视频详情
*/
@GetMapping("/{id}")
public Result<VideoInfoVO> getVideoInfo(@PathVariable Long id, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
return videoService.getVideoInfo(id, userId);
}
/**
* 获取视频列表
*/
@GetMapping("/list")
public Result<Page<VideoListVO>> getVideoList(
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
VideoQueryDTO queryDTO,
HttpServletRequest request) {
Page<ContentVideo> page = new Page<>(pageNum, pageSize);
Long userId = getUserIdFromRequest(request);
return videoService.getVideoList(page, queryDTO, userId);
}
/**
* 更新视频信息
*/
@PutMapping
public Result<Void> updateVideo(@RequestBody @Valid VideoUpdateDTO updateDTO, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.updateVideo(updateDTO, userId);
}
/**
* 删除视频
*/
@DeleteMapping("/{id}")
public Result<Void> deleteVideo(@PathVariable Long id, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.deleteVideo(id, userId);
}
/**
* 点赞视频
*/
@PostMapping("/like/{videoId}")
public Result<Void> likeVideo(@PathVariable Long videoId, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.likeVideo(videoId, userId);
}
/**
* 取消点赞
*/
@DeleteMapping("/like/{videoId}")
public Result<Void> unlikeVideo(@PathVariable Long videoId, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.unlikeVideo(videoId, userId);
}
/**
* 投币
*/
@PostMapping("/coin/{videoId}")
public Result<Void> coinVideo(
@PathVariable Long videoId,
@RequestParam(value = "amount", defaultValue = "1") Integer amount,
HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.coinVideo(videoId, userId, amount);
}
/**
* 收藏视频
*/
@PostMapping("/favorite")
public Result<Void> favoriteVideo(@RequestBody @Valid FavoriteDTO favoriteDTO, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.favoriteVideo(favoriteDTO, userId);
}
/**
* 取消收藏
*/
@DeleteMapping("/favorite/{videoId}")
public Result<Void> unfavoriteVideo(@PathVariable Long videoId, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.unfavoriteVideo(videoId, userId);
}
/**
* 分享视频
*/
@PostMapping("/share/{videoId}")
public Result<Void> shareVideo(
@PathVariable Long videoId,
@RequestParam(value = "platform", required = false) String platform,
HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.shareVideo(videoId, userId, platform);
}
/**
* 播放记录
*/
@PostMapping("/play")
public Result<Void> playVideo(@RequestBody @Valid PlayRecordDTO recordDTO, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return videoService.playVideo(recordDTO, userId);
}
/**
* 获取用户视频列表
*/
@GetMapping("/user/{userId}")
public Result<Page<VideoListVO>> getUserVideos(
@PathVariable Long userId,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest request) {
Page<ContentVideo> page = new Page<>(pageNum, pageSize);
Long currentUserId = getUserIdFromRequest(request);
return videoService.getUserVideos(userId, page, currentUserId);
}
/**
* 视频审核
*/
@PostMapping("/review/{videoId}")
public Result<Void> reviewVideo(
@PathVariable Long videoId,
@RequestParam Integer status,
@RequestParam(required = false) String reason,
HttpServletRequest request) {
Long reviewerId = getUserIdFromRequest(request);
if (reviewerId == null) {
return Result.error("未获取到用户ID");
}
return videoService.reviewVideo(videoId, reviewerId, status, reason);
}
/**
* 从请求中获取用户ID
*/
private Long getUserIdFromRequest(HttpServletRequest request) {
Object userIdObj = request.getAttribute("userId");
if (userIdObj instanceof Long) {
return (Long) userIdObj;
}
return null;
}
}
@@ -0,0 +1,57 @@
package com.nuaa.video.content.controller;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentVideoStatDaily;
import com.nuaa.video.content.service.VideoStatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 视频统计控制器
*/
@Slf4j
@RestController
@RequestMapping("/content/stat")
@RequiredArgsConstructor
public class VideoStatController {
private final VideoStatService videoStatService;
/**
* 获取视频每日统计
*/
@GetMapping("/daily/{videoId}")
public Result<List<ContentVideoStatDaily>> getVideoStatDaily(
@PathVariable Long videoId,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
return videoStatService.getVideoStatDaily(videoId, startDate, endDate);
}
/**
* 获取视频播放趋势(按小时)
*/
@GetMapping("/play/trend/{videoId}")
public Result<Map<Integer, Long>> getVideoPlayTrend(
@PathVariable Long videoId,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
if (date == null) {
date = LocalDate.now();
}
return videoStatService.getVideoPlayTrend(videoId, date);
}
/**
* 获取视频互动统计
*/
@GetMapping("/interaction/{videoId}")
public Result<Map<String, Object>> getVideoInteractionStats(@PathVariable Long videoId) {
return videoStatService.getVideoInteractionStats(videoId);
}
}
@@ -0,0 +1,48 @@
package com.nuaa.video.content.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
/**
* 弹幕DTO
*/
@Data
public class DanmakuDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
@NotNull(message = "视频ID不能为空")
private Long videoId;
/**
* 弹幕内容
*/
@NotBlank(message = "弹幕内容不能为空")
private String content;
/**
* 弹幕颜色
*/
private String color = "#FFFFFF";
/**
* 字体大小
*/
private Integer fontSize = 25;
/**
* 位置(0:滚动,1:顶部,2:底部)
*/
private Integer position = 0;
/**
* 发送时间(秒)
*/
@NotNull(message = "发送时间不能为空")
private Float time;
}
@@ -0,0 +1,26 @@
package com.nuaa.video.content.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
/**
* 收藏DTO
*/
@Data
public class FavoriteDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
@NotNull(message = "视频ID不能为空")
private Long videoId;
/**
* 收藏夹ID
*/
@NotNull(message = "收藏夹ID不能为空")
private Long favoriteId;
}
@@ -0,0 +1,31 @@
package com.nuaa.video.content.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
/**
* 播放记录DTO
*/
@Data
public class PlayRecordDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
@NotNull(message = "视频ID不能为空")
private Long videoId;
/**
* 播放时长(秒)
*/
@NotNull(message = "播放时长不能为空")
private Integer playTime;
/**
* 是否看完(0:未看完,1:已看完)
*/
private Integer isFinished;
}
@@ -0,0 +1,69 @@
package com.nuaa.video.content.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 视频查询DTO
*/
@Data
public class VideoQueryDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 关键词
*/
private String keyword;
/**
* 分类ID
*/
private Long categoryId;
/**
* 标签ID列表
*/
private List<Long> tagIds;
/**
* 用户ID
*/
private Long userId;
/**
* 排序字段
*/
private String orderBy;
/**
* 排序方向
*/
private String orderDirection;
/**
* 状态
*/
private Integer status;
/**
* 是否推荐
*/
private Integer isRecommend;
/**
* 是否置顶
*/
private Integer isTop;
/**
* 当前页码
*/
private Integer pageNum = 1;
/**
* 每页记录数
*/
private Integer pageSize = 10;
}
@@ -0,0 +1,47 @@
package com.nuaa.video.content.dto;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 视频更新DTO
*/
@Data
public class VideoUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
private Long id;
/**
* 视频标题
*/
@Size(min = 5, max = 100, message = "视频标题长度必须在5-100之间")
private String title;
/**
* 视频描述
*/
@Size(max = 1000, message = "视频描述长度不能超过1000")
private String description;
/**
* 分类ID
*/
private Long categoryId;
/**
* 标签ID列表
*/
private List<Long> tagIds;
/**
* 封面URL
*/
private String coverUrl;
}
@@ -0,0 +1,76 @@
package com.nuaa.video.content.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 视频上传DTO
*/
@Data
public class VideoUploadDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频标题
*/
@NotBlank(message = "视频标题不能为空")
@Size(min = 5, max = 100, message = "视频标题长度必须在5-100之间")
private String title;
/**
* 视频描述
*/
@Size(max = 1000, message = "视频描述长度不能超过1000")
private String description;
/**
* 分类ID
*/
@NotNull(message = "分类ID不能为空")
private Long categoryId;
/**
* 标签ID列表
*/
private List<Long> tagIds;
/**
* 封面URL
*/
private String coverUrl;
/**
* 视频URL
*/
private String videoUrl;
/**
* 视频时长(秒)
*/
private Integer duration;
/**
* 视频宽度
*/
private Integer width;
/**
* 视频高度
*/
private Integer height;
/**
* 视频格式
*/
private String format;
/**
* 视频大小(字节)
*/
private Long size;
}
@@ -0,0 +1,79 @@
package com.nuaa.video.content.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("content_category")
public class ContentCategory implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分类ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 分类名称
*/
private String name;
/**
* 分类编码
*/
private String code;
/**
* 父分类ID
*/
private Long parentId;
/**
* 分类层级
*/
private Integer level;
/**
* 排序
*/
private Integer sort;
/**
* 图标
*/
private String icon;
/**
* 状态(0:禁用,1:正常)
*/
private Integer status;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,64 @@
package com.nuaa.video.content.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("content_danmaku")
public class ContentDanmaku 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 userId;
/**
* 弹幕内容
*/
private String content;
/**
* 弹幕颜色
*/
private String color;
/**
* 字体大小
*/
private Integer fontSize;
/**
* 位置(0:滚动,1:顶部,2:底部)
*/
private Integer position;
/**
* 发送时间(秒)
*/
private Float time;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,49 @@
package com.nuaa.video.content.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("content_tag")
public class ContentTag implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 标签ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 标签名称
*/
private String name;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,69 @@
package com.nuaa.video.content.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("content_user_favorite")
public class ContentUserFavorite implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 收藏夹ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 收藏夹名称
*/
private String name;
/**
* 收藏夹描述
*/
private String description;
/**
* 是否私有(0:公开,1:私有)
*/
private Integer isPrivate;
/**
* 视频数量
*/
private Integer videoCount;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,204 @@
package com.nuaa.video.content.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 视频信息实体
*/
@Data
@TableName("content_video")
public class ContentVideo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 视频标题
*/
private String title;
/**
* 视频描述
*/
private String description;
/**
* 封面URL
*/
private String coverUrl;
/**
* 视频URL
*/
private String videoUrl;
/**
* 视频时长(秒)
*/
private Integer duration;
/**
* 视频宽度
*/
private Integer width;
/**
* 视频高度
*/
private Integer height;
/**
* 视频格式
*/
private String format;
/**
* 视频大小(字节)
*/
private Long size;
/**
* 分类ID
*/
private Long categoryId;
/**
* 状态(0:审核中,1:已发布,2:已下架,3:审核不通过)
*/
private Integer status;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 投币数
*/
private Integer coinCount;
/**
* 收藏数
*/
private Integer favoriteCount;
/**
* 评论数
*/
private Integer commentCount;
/**
* 播放数
*/
private Long viewCount;
/**
* 分享数
*/
private Integer shareCount;
/**
* 弹幕数
*/
private Integer danmakuCount;
/**
* 是否推荐(0:否,1:是)
*/
private Integer isRecommend;
/**
* 是否置顶(0:否,1:是)
*/
private Integer isTop;
/**
* 发布时间
*/
private LocalDateTime publishTime;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 标签ID列表(非数据库字段)
*/
@TableField(exist = false)
private List<Long> tagIds;
/**
* 标签名称列表(非数据库字段)
*/
@TableField(exist = false)
private List<String> tagNames;
/**
* 分类名称(非数据库字段)
*/
@TableField(exist = false)
private String categoryName;
/**
* 用户昵称(非数据库字段)
*/
@TableField(exist = false)
private String userNickname;
/**
* 用户头像(非数据库字段)
*/
@TableField(exist = false)
private String userAvatar;
/**
* 是否已点赞(非数据库字段)
*/
@TableField(exist = false)
private Boolean isLiked;
/**
* 是否已收藏(非数据库字段)
*/
@TableField(exist = false)
private Boolean isFavorited;
/**
* 是否已投币(非数据库字段)
*/
@TableField(exist = false)
private Boolean isCoined;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.content.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("content_video_coin")
public class ContentVideoCoin 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 userId;
/**
* 投币数量
*/
private Integer amount;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.content.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("content_video_favorite")
public class ContentVideoFavorite 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 userId;
/**
* 收藏夹ID
*/
private Long favoriteId;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,39 @@
package com.nuaa.video.content.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("content_video_like")
public class ContentVideoLike 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 userId;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,54 @@
package com.nuaa.video.content.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("content_video_play_record")
public class ContentVideoPlayRecord 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 userId;
/**
* 播放时长(秒)
*/
private Integer playTime;
/**
* 是否看完(0:未看完,1:已看完)
*/
private Integer isFinished;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,54 @@
package com.nuaa.video.content.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("content_video_review")
public class ContentVideoReview 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 reviewerId;
/**
* 审核状态(1:通过,2:不通过)
*/
private Integer status;
/**
* 审核原因
*/
private String reason;
/**
* 创建人
*/
private String createdBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,75 @@
package com.nuaa.video.content.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.LocalDate;
import java.time.LocalDateTime;
/**
* 视频每日统计实体
*/
@Data
@TableName("content_video_stat_daily")
public class ContentVideoStatDaily implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 视频ID
*/
private Long videoId;
/**
* 统计日期
*/
private LocalDate statDate;
/**
* 新增点赞数
*/
private Integer likeIncrement;
/**
* 新增投币数
*/
private Integer coinIncrement;
/**
* 新增收藏数
*/
private Integer favoriteIncrement;
/**
* 新增评论数
*/
private Integer commentIncrement;
/**
* 新增播放数
*/
private Long viewIncrement;
/**
* 新增分享数
*/
private Integer shareIncrement;
/**
* 新增弹幕数
*/
private Integer danmakuIncrement;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.content.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("content_video_tag")
public class ContentVideoTag 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 tagId;
/**
* 创建人
*/
private String createdBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,31 @@
package com.nuaa.video.content.enums;
import lombok.Getter;
/**
* 弹幕位置枚举
*/
@Getter
public enum DanmakuPositionEnum {
ROLLING(0, "滚动"),
TOP(1, "顶部"),
BOTTOM(2, "底部");
private final Integer code;
private final String desc;
DanmakuPositionEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public static DanmakuPositionEnum getByCode(Integer code) {
for (DanmakuPositionEnum positionEnum : values()) {
if (positionEnum.getCode().equals(code)) {
return positionEnum;
}
}
return ROLLING; // 默认滚动
}
}
@@ -0,0 +1,32 @@
package com.nuaa.video.content.enums;
import lombok.Getter;
/**
* 视频状态枚举
*/
@Getter
public enum VideoStatusEnum {
REVIEWING(0, "审核中"),
PUBLISHED(1, "已发布"),
DISABLED(2, "已下架"),
REJECTED(3, "审核不通过");
private final Integer code;
private final String desc;
VideoStatusEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public static VideoStatusEnum getByCode(Integer code) {
for (VideoStatusEnum statusEnum : values()) {
if (statusEnum.getCode().equals(code)) {
return statusEnum;
}
}
return null;
}
}
@@ -0,0 +1,60 @@
package com.nuaa.video.content.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.PostMapping;
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 userId 用户ID
* @param count 增加数量
* @return 结果
*/
@PostMapping("/user/profile/count/video/{userId}/{count}")
Result<Void> increaseVideoCount(@PathVariable("userId") Long userId,
@PathVariable("count") Integer count,
@RequestHeader("Authorization") String token);
/**
* 增加用户获赞数量
*
* @param userId 用户ID
* @param count 增加数量
* @return 结果
*/
@PostMapping("/user/profile/count/like/{userId}/{count}")
Result<Void> increaseLikeCount(@PathVariable("userId") Long userId,
@PathVariable("count") Integer count,
@RequestHeader("Authorization") String token);
/**
* 增加用户收藏数量
*
* @param userId 用户ID
* @param count 增加数量
* @return 结果
*/
@PostMapping("/user/profile/count/favorite/{userId}/{count}")
Result<Void> increaseFavoriteCount(@PathVariable("userId") Long userId,
@PathVariable("count") Integer count,
@RequestHeader("Authorization") String token);
}
@@ -0,0 +1,44 @@
package com.nuaa.video.content.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<Void> increaseVideoCount(Long userId, Integer count, String token) {
log.error("增加用户视频数量失败,用户ID:{}, 数量:{}", userId, count);
return Result.error("更新用户视频数量失败");
}
@Override
public Result<Void> increaseLikeCount(Long userId, Integer count, String token) {
log.error("增加用户获赞数量失败,用户ID:{}, 数量:{}", userId, count);
return Result.error("更新用户获赞数量失败");
}
@Override
public Result<Void> increaseFavoriteCount(Long userId, Integer count, String token) {
log.error("增加用户收藏数量失败,用户ID:{}, 数量:{}", userId, count);
return Result.error("更新用户收藏数量失败");
}
};
}
}
@@ -0,0 +1,102 @@
package com.nuaa.video.content.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);
}
}
@@ -0,0 +1,12 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentCategory;
import org.apache.ibatis.annotations.Mapper;
/**
* 视频分类Mapper
*/
@Mapper
public interface ContentCategoryMapper extends BaseMapper<ContentCategory> {
}
@@ -0,0 +1,28 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentDanmaku;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 弹幕Mapper
*/
@Mapper
public interface ContentDanmakuMapper extends BaseMapper<ContentDanmaku> {
/**
* 获取视频指定时间段的弹幕
*
* @param videoId 视频ID
* @param startTime 开始时间(秒)
* @param endTime 结束时间(秒)
* @return 弹幕列表
*/
List<ContentDanmaku> getVideoTimeDanmaku(
@Param("videoId") Long videoId,
@Param("startTime") Float startTime,
@Param("endTime") Float endTime);
}
@@ -0,0 +1,23 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentTag;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 视频标签Mapper
*/
@Mapper
public interface ContentTagMapper extends BaseMapper<ContentTag> {
/**
* 获取视频的标签
*
* @param videoId 视频ID
* @return 标签列表
*/
List<ContentTag> getTagsByVideoId(@Param("videoId") Long videoId);
}
@@ -0,0 +1,22 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentUserFavorite;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 用户收藏夹Mapper
*/
@Mapper
public interface ContentUserFavoriteMapper extends BaseMapper<ContentUserFavorite> {
/**
* 增加收藏夹视频数量
*
* @param favoriteId 收藏夹ID
* @param count 增加数量
* @return 影响行数
*/
int increaseVideoCount(@Param("favoriteId") Long favoriteId, @Param("count") int count);
}
@@ -0,0 +1,22 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoCoin;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 视频投币Mapper
*/
@Mapper
public interface ContentVideoCoinMapper extends BaseMapper<ContentVideoCoin> {
/**
* 获取用户对视频的投币数量
*
* @param userId 用户ID
* @param videoId 视频ID
* @return 投币数量
*/
int getUserCoinCount(@Param("userId") Long userId, @Param("videoId") Long videoId);
}
@@ -0,0 +1,22 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoFavorite;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 视频收藏Mapper
*/
@Mapper
public interface ContentVideoFavoriteMapper extends BaseMapper<ContentVideoFavorite> {
/**
* 检查用户是否已收藏视频
*
* @param userId 用户ID
* @param videoId 视频ID
* @return 是否已收藏
*/
int checkUserFavorited(@Param("userId") Long userId, @Param("videoId") Long videoId);
}
@@ -0,0 +1,22 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoLike;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 视频点赞Mapper
*/
@Mapper
public interface ContentVideoLikeMapper extends BaseMapper<ContentVideoLike> {
/**
* 检查用户是否已点赞视频
*
* @param userId 用户ID
* @param videoId 视频ID
* @return 是否已点赞
*/
int checkUserLiked(@Param("userId") Long userId, @Param("videoId") Long videoId);
}
@@ -0,0 +1,88 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.content.dto.VideoQueryDTO;
import com.nuaa.video.content.entity.ContentVideo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 视频信息Mapper
*/
@Mapper
public interface ContentVideoMapper extends BaseMapper<ContentVideo> {
/**
* 分页查询视频列表
*
* @param page 分页参数
* @param queryDTO 查询条件
* @return 视频列表
*/
Page<ContentVideo> selectVideoList(Page<ContentVideo> page, @Param("query") VideoQueryDTO queryDTO);
/**
* 增加点赞数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseLikeCount(@Param("videoId") Long videoId, @Param("count") int count);
/**
* 增加投币数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseCoinCount(@Param("videoId") Long videoId, @Param("count") int count);
/**
* 增加收藏数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseFavoriteCount(@Param("videoId") Long videoId, @Param("count") int count);
/**
* 增加评论数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseCommentCount(@Param("videoId") Long videoId, @Param("count") int count);
/**
* 增加播放数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseViewCount(@Param("videoId") Long videoId, @Param("count") long count);
/**
* 增加分享数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseShareCount(@Param("videoId") Long videoId, @Param("count") int count);
/**
* 增加弹幕数
*
* @param videoId 视频ID
* @param count 增加数量
* @return 影响行数
*/
int increaseDanmakuCount(@Param("videoId") Long videoId, @Param("count") int count);
}
@@ -0,0 +1,38 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoPlayRecord;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 视频播放记录Mapper
*/
@Mapper
public interface ContentVideoPlayRecordMapper extends BaseMapper<ContentVideoPlayRecord> {
/**
* 获取用户播放过的视频ID列表
*
* @param userId 用户ID
* @param limit 限制数量
* @return 视频ID列表
*/
List<Long> selectUserPlayedVideoIds(@Param("userId") Long userId, @Param("limit") Integer limit);
/**
* 统计按小时的播放量
*
* @param videoId 视频ID
* @param date 日期
* @return 每小时播放量
*/
@MapKey("hour")
List<Map<String, Object>> countHourlyPlays(@Param("videoId") Long videoId, @Param("date") LocalDate date);
}
@@ -0,0 +1,32 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoReview;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 视频审核记录Mapper
*/
@Mapper
public interface ContentVideoReviewMapper extends BaseMapper<ContentVideoReview> {
/**
* 查询视频最新审核记录
*
* @param videoId 视频ID
* @return 审核记录
*/
ContentVideoReview selectLatestReview(@Param("videoId") Long videoId);
/**
* 根据审核状态查询视频审核记录
*
* @param status 审核状态
* @param reviewerId 审核员ID(可为null)
* @return 审核记录列表
*/
List<ContentVideoReview> selectReviewsByStatus(@Param("status") Integer status, @Param("reviewerId") Long reviewerId);
}
@@ -0,0 +1,38 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoStatDaily;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 视频每日统计Mapper
*/
@Mapper
public interface ContentVideoStatDailyMapper extends BaseMapper<ContentVideoStatDaily> {
/**
* 获取视频日期范围内的统计数据
*
* @param videoId 视频ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 统计数据列表
*/
List<ContentVideoStatDaily> selectStatsByDateRange(
@Param("videoId") Long videoId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
/**
* 获取视频总互动统计
*
* @param videoId 视频ID
* @return 互动统计信息
*/
Map<String, Object> selectVideoInteractionStats(@Param("videoId") Long videoId);
}
@@ -0,0 +1,25 @@
package com.nuaa.video.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.content.entity.ContentVideoTag;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 视频标签关联Mapper
*/
@Mapper
public interface ContentVideoTagMapper extends BaseMapper<ContentVideoTag> {
/**
* 批量插入视频标签关联
*
* @param videoId 视频ID
* @param tagIds 标签ID列表
* @param createdBy 创建人
* @return 影响行数
*/
int batchInsert(@Param("videoId") Long videoId, @Param("tagIds") List<Long> tagIds, @Param("createdBy") String createdBy);
}
@@ -0,0 +1,235 @@
package com.nuaa.video.content.mq.consumer;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.entity.ContentVideoStatDaily;
import com.nuaa.video.content.mapper.ContentVideoMapper;
import com.nuaa.video.content.mapper.ContentVideoStatDailyMapper;
import com.nuaa.video.content.mq.producer.VideoMessageProducer;
import com.nuaa.video.content.service.VideoStatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 视频消息消费者
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class VideoMessageConsumer {
private final ContentVideoMapper videoMapper;
private final ContentVideoStatDailyMapper statDailyMapper;
private final VideoStatService videoStatService;
/**
* 视频统计消息监听器
*/
@Component
@RocketMQMessageListener(
topic = "video-stats-topic",
consumerGroup = "video-stats-consumer-group",
consumeMode = ConsumeMode.CONCURRENTLY
)
@RequiredArgsConstructor
class VideoStatsListener implements RocketMQListener<VideoMessageProducer.VideoStatsMessage> {
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(VideoMessageProducer.VideoStatsMessage message) {
try {
log.info("接收到视频统计消息: {}", message);
Long videoId = message.getVideoId();
String action = message.getAction();
Integer count = message.getCount() == null ? 1 : message.getCount();
// 更新视频统计数据
switch (action) {
case "play":
videoMapper.increaseViewCount(videoId, count);
break;
case "like":
videoMapper.increaseLikeCount(videoId, count);
break;
case "favorite":
videoMapper.increaseFavoriteCount(videoId, count);
break;
case "coin":
videoMapper.increaseCoinCount(videoId, count);
break;
case "share":
videoMapper.increaseShareCount(videoId, count);
break;
case "danmaku":
videoMapper.increaseDanmakuCount(videoId, count);
break;
case "comment":
videoMapper.increaseCommentCount(videoId, count);
break;
default:
log.warn("未知的统计动作: {}", action);
}
// 更新每日统计数据
updateDailyStats(videoId, action, count);
} catch (Exception e) {
log.error("处理视频统计消息失败", e);
throw e;
}
}
/**
* 更新每日统计数据
*/
private void updateDailyStats(Long videoId, String action, int count) {
LocalDate today = LocalDate.now();
// 查询今日统计记录
ContentVideoStatDaily statDaily = statDailyMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ContentVideoStatDaily>()
.eq(ContentVideoStatDaily::getVideoId, videoId)
.eq(ContentVideoStatDaily::getStatDate, today)
);
// 如果不存在,创建新记录
if (statDaily == null) {
statDaily = new ContentVideoStatDaily();
statDaily.setVideoId(videoId);
statDaily.setStatDate(today);
statDaily.setLikeIncrement(0);
statDaily.setCoinIncrement(0);
statDaily.setFavoriteIncrement(0);
statDaily.setCommentIncrement(0);
statDaily.setViewIncrement(0L);
statDaily.setShareIncrement(0);
statDaily.setDanmakuIncrement(0);
statDaily.setCreateTime(LocalDateTime.now());
}
// 更新统计数据
switch (action) {
case "play":
statDaily.setViewIncrement(statDaily.getViewIncrement() + count);
break;
case "like":
statDaily.setLikeIncrement(statDaily.getLikeIncrement() + count);
break;
case "favorite":
statDaily.setFavoriteIncrement(statDaily.getFavoriteIncrement() + count);
break;
case "coin":
statDaily.setCoinIncrement(statDaily.getCoinIncrement() + count);
break;
case "share":
statDaily.setShareIncrement(statDaily.getShareIncrement() + count);
break;
case "danmaku":
statDaily.setDanmakuIncrement(statDaily.getDanmakuIncrement() + count);
break;
case "comment":
statDaily.setCommentIncrement(statDaily.getCommentIncrement() + count);
break;
}
// 保存更新
if (statDaily.getId() == null) {
statDailyMapper.insert(statDaily);
} else {
statDailyMapper.updateById(statDaily);
}
}
}
/**
* 视频处理消息监听器
*/
@Component
@RocketMQMessageListener(
topic = "video-process-topic",
consumerGroup = "video-process-consumer-group",
consumeMode = ConsumeMode.CONCURRENTLY
)
@RequiredArgsConstructor
class VideoProcessListener implements RocketMQListener<Long> {
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(Long videoId) {
try {
log.info("接收到视频处理消息: videoId={}", videoId);
// 查询视频信息
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
log.error("视频不存在: videoId={}", videoId);
return;
}
// 视频处理逻辑(实际中可能涉及转码、视频分析等)
log.info("处理视频: videoId={}", videoId);
// 更新视频状态为待审核
video.setStatus(0); // 待审核
videoMapper.updateById(video);
// 发送审核消息
VideoMessageProducer videoMessageProducer = new VideoMessageProducer(null);
videoMessageProducer.sendVideoReviewMessage(videoId);
} catch (Exception e) {
log.error("处理视频处理消息失败", e);
throw e;
}
}
}
/**
* 视频审核消息监听器(自动审核)
*/
@Component
@RocketMQMessageListener(
topic = "video-review-topic",
consumerGroup = "video-review-consumer-group",
consumeMode = ConsumeMode.CONCURRENTLY
)
@RequiredArgsConstructor
class VideoReviewListener implements RocketMQListener<Long> {
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(Long videoId) {
try {
log.info("接收到视频审核消息: videoId={}", videoId);
// 查询视频信息
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
log.error("视频不存在: videoId={}", videoId);
return;
}
// 自动审核逻辑(实际中可能涉及内容识别、AI审核等)
// 这里简化为直接通过
log.info("审核视频: videoId={}", videoId);
// 更新视频状态为已发布
video.setStatus(1); // 已发布
video.setPublishTime(LocalDateTime.now());
videoMapper.updateById(video);
} catch (Exception e) {
log.error("处理视频审核消息失败", e);
throw e;
}
}
}
}
@@ -0,0 +1,75 @@
package com.nuaa.video.content.mq.producer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
/**
* 视频消息生产者
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class VideoMessageProducer {
private final RocketMQTemplate rocketMQTemplate;
private static final String VIDEO_UPLOAD_TOPIC = "video-upload-topic";
private static final String VIDEO_PROCESS_TOPIC = "video-process-topic";
private static final String VIDEO_REVIEW_TOPIC = "video-review-topic";
private static final String VIDEO_STATS_TOPIC = "video-stats-topic";
/**
* 发送视频上传消息
*
* @param videoId 视频ID
*/
public void sendVideoUploadMessage(Long videoId) {
log.info("发送视频上传消息: videoId={}", videoId);
rocketMQTemplate.convertAndSend(VIDEO_UPLOAD_TOPIC, videoId);
}
/**
* 发送视频处理消息
*
* @param videoId 视频ID
*/
public void sendVideoProcessMessage(Long videoId) {
log.info("发送视频处理消息: videoId={}", videoId);
rocketMQTemplate.convertAndSend(VIDEO_PROCESS_TOPIC, videoId);
}
/**
* 发送视频审核消息
*
* @param videoId 视频ID
*/
public void sendVideoReviewMessage(Long videoId) {
log.info("发送视频审核消息: videoId={}", videoId);
rocketMQTemplate.convertAndSend(VIDEO_REVIEW_TOPIC, videoId);
}
/**
* 发送视频播放统计消息
*
* @param message 统计消息
*/
public void sendVideoStatsMessage(VideoStatsMessage message) {
log.info("发送视频统计消息: videoId={}, userId={}, action={}",
message.getVideoId(), message.getUserId(), message.getAction());
rocketMQTemplate.send(VIDEO_STATS_TOPIC, MessageBuilder.withPayload(message).build());
}
/**
* 视频统计消息
*/
@lombok.Data
public static class VideoStatsMessage {
private Long videoId;
private Long userId;
private String action; // play, like, favorite, coin, share, danmaku
private Integer count;
}
}
@@ -0,0 +1,52 @@
package com.nuaa.video.content.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentCategory;
import com.nuaa.video.content.vo.CategoryVO;
import java.util.List;
/**
* 分类服务接口
*/
public interface CategoryService extends IService<ContentCategory> {
/**
* 获取分类树
*
* @return 分类树
*/
Result<List<CategoryVO>> getCategoryTree();
/**
* 获取分类列表
*
* @return 分类列表
*/
Result<List<CategoryVO>> getCategoryList();
/**
* 添加分类
*
* @param category 分类信息
* @return 结果
*/
Result<Void> addCategory(ContentCategory category);
/**
* 更新分类
*
* @param category 分类信息
* @return 结果
*/
Result<Void> updateCategory(ContentCategory category);
/**
* 删除分类
*
* @param id 分类ID
* @return 结果
*/
Result<Void> deleteCategory(Long id);
}
@@ -0,0 +1,42 @@
package com.nuaa.video.content.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.dto.DanmakuDTO;
import com.nuaa.video.content.entity.ContentDanmaku;
import com.nuaa.video.content.vo.DanmakuVO;
import java.util.List;
/**
* 弹幕服务接口
*/
public interface DanmakuService extends IService<ContentDanmaku> {
/**
* 发送弹幕
*
* @param danmakuDTO 弹幕信息
* @param userId 用户ID
* @return 结果
*/
Result<Long> sendDanmaku(DanmakuDTO danmakuDTO, Long userId);
/**
* 获取视频指定时间段的弹幕
*
* @param videoId 视频ID
* @param startTime 开始时间(秒)
* @param endTime 结束时间(秒)
* @return 弹幕列表
*/
Result<List<DanmakuVO>> getVideoTimeDanmaku(Long videoId, Float startTime, Float endTime);
/**
* 获取视频所有弹幕
*
* @param videoId 视频ID
* @return 弹幕列表
*/
Result<List<DanmakuVO>> getVideoAllDanmaku(Long videoId);
}
@@ -0,0 +1,62 @@
package com.nuaa.video.content.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentUserFavorite;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.vo.FavoriteVO;
import com.nuaa.video.content.vo.VideoListVO;
import java.util.List;
/**
* 收藏夹服务接口
*/
public interface FavoriteService extends IService<ContentUserFavorite> {
/**
* 获取用户收藏夹列表
*
* @param userId 用户ID
* @return 收藏夹列表
*/
Result<List<FavoriteVO>> getUserFavorites(Long userId);
/**
* 创建收藏夹
*
* @param favorite 收藏夹信息
* @param userId 用户ID
* @return 收藏夹ID
*/
Result<Long> createFavorite(ContentUserFavorite favorite, Long userId);
/**
* 更新收藏夹
*
* @param favorite 收藏夹信息
* @param userId 用户ID
* @return 结果
*/
Result<Void> updateFavorite(ContentUserFavorite favorite, Long userId);
/**
* 删除收藏夹
*
* @param id 收藏夹ID
* @param userId 用户ID
* @return 结果
*/
Result<Void> deleteFavorite(Long id, Long userId);
/**
* 获取收藏夹中的视频
*
* @param favoriteId 收藏夹ID
* @param page 分页参数
* @param userId 用户ID
* @return 视频列表
*/
Result<Page<VideoListVO>> getFavoriteVideos(Long favoriteId, Page<ContentVideo> page, Long userId);
}
@@ -0,0 +1,63 @@
package com.nuaa.video.content.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentTag;
import com.nuaa.video.content.vo.TagVO;
import java.util.List;
/**
* 标签服务接口
*/
public interface TagService extends IService<ContentTag> {
/**
* 获取标签列表
*
* @return 标签列表
*/
Result<List<TagVO>> getTagList();
/**
* 分页获取标签列表
*
* @param page 分页参数
* @param keyword 关键词
* @return 分页标签列表
*/
Result<Page<TagVO>> getTagPage(Page<ContentTag> page, String keyword);
/**
* 添加标签
*
* @param tag 标签信息
* @return 结果
*/
Result<Void> addTag(ContentTag tag);
/**
* 更新标签
*
* @param tag 标签信息
* @return 结果
*/
Result<Void> updateTag(ContentTag tag);
/**
* 删除标签
*
* @param id 标签ID
* @return 结果
*/
Result<Void> deleteTag(Long id);
/**
* 获取视频的标签
*
* @param videoId 视频ID
* @return 标签列表
*/
Result<List<TagVO>> getVideoTags(Long videoId);
}
@@ -0,0 +1,158 @@
package com.nuaa.video.content.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.dto.*;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.vo.UploadResultVO;
import com.nuaa.video.content.vo.VideoInfoVO;
import com.nuaa.video.content.vo.VideoListVO;
import org.springframework.web.multipart.MultipartFile;
/**
* 视频服务接口
*/
public interface VideoService extends IService<ContentVideo> {
/**
* 上传视频
*
* @param videoFile 视频文件
* @param userId 用户ID
* @return 上传结果
*/
Result<UploadResultVO> uploadVideo(MultipartFile videoFile, Long userId);
/**
* 创建视频
*
* @param uploadDTO 视频信息DTO
* @param userId 用户ID
* @return 视频ID
*/
Result<Long> createVideo(VideoUploadDTO uploadDTO, Long userId);
/**
* 获取视频详情
*
* @param id 视频ID
* @param userId 当前用户ID
* @return 视频详情
*/
Result<VideoInfoVO> getVideoInfo(Long id, Long userId);
/**
* 分页查询视频列表
*
* @param page 分页参数
* @param queryDTO 查询条件
* @param userId 当前用户ID
* @return 视频列表
*/
Result<Page<VideoListVO>> getVideoList(Page<ContentVideo> page, VideoQueryDTO queryDTO, Long userId);
/**
* 更新视频信息
*
* @param updateDTO 更新信息
* @param userId 用户ID
* @return 结果
*/
Result<Void> updateVideo(VideoUpdateDTO updateDTO, Long userId);
/**
* 删除视频
*
* @param id 视频ID
* @param userId 用户ID
* @return 结果
*/
Result<Void> deleteVideo(Long id, Long userId);
/**
* 点赞视频
*
* @param videoId 视频ID
* @param userId 用户ID
* @return 结果
*/
Result<Void> likeVideo(Long videoId, Long userId);
/**
* 取消点赞
*
* @param videoId 视频ID
* @param userId 用户ID
* @return 结果
*/
Result<Void> unlikeVideo(Long videoId, Long userId);
/**
* 投币
*
* @param videoId 视频ID
* @param userId 用户ID
* @param amount 投币数量
* @return 结果
*/
Result<Void> coinVideo(Long videoId, Long userId, Integer amount);
/**
* 收藏视频
*
* @param favoriteDTO 收藏信息
* @param userId 用户ID
* @return 结果
*/
Result<Void> favoriteVideo(FavoriteDTO favoriteDTO, Long userId);
/**
* 取消收藏
*
* @param videoId 视频ID
* @param userId 用户ID
* @return 结果
*/
Result<Void> unfavoriteVideo(Long videoId, Long userId);
/**
* 分享视频
*
* @param videoId 视频ID
* @param userId 用户ID
* @param platform 分享平台
* @return 结果
*/
Result<Void> shareVideo(Long videoId, Long userId, String platform);
/**
* 播放视频(记录播放记录)
*
* @param recordDTO 播放记录
* @param userId 用户ID
* @return 结果
*/
Result<Void> playVideo(PlayRecordDTO recordDTO, Long userId);
/**
* 获取用户视频列表
*
* @param userId 用户ID
* @param page 分页参数
* @param currentUserId 当前用户ID
* @return 视频列表
*/
Result<Page<VideoListVO>> getUserVideos(Long userId, Page<ContentVideo> page, Long currentUserId);
/**
* 审核视频
*
* @param videoId 视频ID
* @param reviewerId 审核员ID
* @param status 审核状态
* @param reason 审核原因
* @return 结果
*/
Result<Void> reviewVideo(Long videoId, Long reviewerId, Integer status, String reason);
}
@@ -0,0 +1,41 @@
package com.nuaa.video.content.service;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentVideoStatDaily;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 视频统计服务接口
*/
public interface VideoStatService {
/**
* 获取视频每日统计
*
* @param videoId 视频ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 统计数据
*/
Result<List<ContentVideoStatDaily>> getVideoStatDaily(Long videoId, LocalDate startDate, LocalDate endDate);
/**
* 获取视频播放趋势(按小时)
*
* @param videoId 视频ID
* @param date 日期
* @return 播放趋势
*/
Result<Map<Integer, Long>> getVideoPlayTrend(Long videoId, LocalDate date);
/**
* 获取视频互动统计
*
* @param videoId 视频ID
* @return 互动统计
*/
Result<Map<String, Object>> getVideoInteractionStats(Long videoId);
}
@@ -0,0 +1,192 @@
package com.nuaa.video.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentCategory;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.mapper.ContentCategoryMapper;
import com.nuaa.video.content.mapper.ContentVideoMapper;
import com.nuaa.video.content.service.CategoryService;
import com.nuaa.video.content.vo.CategoryVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 分类服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CategoryServiceImpl extends ServiceImpl<ContentCategoryMapper, ContentCategory> implements CategoryService {
private final ContentCategoryMapper categoryMapper;
private final ContentVideoMapper videoMapper;
@Override
public Result<List<CategoryVO>> getCategoryTree() {
// 查询所有分类
List<ContentCategory> categories = categoryMapper.selectList(
new LambdaQueryWrapper<ContentCategory>()
.eq(ContentCategory::getStatus, 1)
.orderByAsc(ContentCategory::getSort)
);
// 将分类列表转换为树形结构
List<CategoryVO> rootCategories = buildCategoryTree(categories, 0L);
return Result.success(rootCategories);
}
/**
* 构建分类树
*/
private List<CategoryVO> buildCategoryTree(List<ContentCategory> categories, Long parentId) {
return categories.stream()
.filter(category -> category.getParentId().equals(parentId))
.map(category -> {
CategoryVO categoryVO = new CategoryVO();
BeanUtils.copyProperties(category, categoryVO);
// 递归获取子分类
List<CategoryVO> children = buildCategoryTree(categories, category.getId());
categoryVO.setChildren(children);
return categoryVO;
})
.sorted(Comparator.comparing(CategoryVO::getSort))
.collect(Collectors.toList());
}
@Override
public Result<List<CategoryVO>> getCategoryList() {
// 查询所有分类
List<ContentCategory> categories = categoryMapper.selectList(
new LambdaQueryWrapper<ContentCategory>()
.eq(ContentCategory::getStatus, 1)
.orderByAsc(ContentCategory::getSort)
);
// 将分类列表转换为VO
List<CategoryVO> categoryVOList = categories.stream()
.map(category -> {
CategoryVO categoryVO = new CategoryVO();
BeanUtils.copyProperties(category, categoryVO);
return categoryVO;
})
.collect(Collectors.toList());
return Result.success(categoryVOList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> addCategory(ContentCategory category) {
// 检查编码是否已存在
long count = count(new LambdaQueryWrapper<ContentCategory>()
.eq(ContentCategory::getCode, category.getCode()));
if (count > 0) {
return Result.error("分类编码已存在");
}
// 设置默认值
if (category.getParentId() == null) {
category.setParentId(0L); // 默认为根分类
}
if (category.getLevel() == null) {
// 如果是根分类,层级为1,否则为父分类层级+1
if (category.getParentId() == 0L) {
category.setLevel(1);
} else {
ContentCategory parentCategory = getById(category.getParentId());
if (parentCategory != null) {
category.setLevel(parentCategory.getLevel() + 1);
} else {
category.setLevel(1);
}
}
}
if (category.getStatus() == null) {
category.setStatus(1); // 默认启用
}
if (category.getSort() == null) {
category.setSort(0); // 默认排序值
}
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
// 保存分类
save(category);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateCategory(ContentCategory category) {
// 检查分类是否存在
ContentCategory existingCategory = getById(category.getId());
if (existingCategory == null) {
return Result.error("分类不存在");
}
// 检查编码是否已存在(排除自身)
if (category.getCode() != null && !category.getCode().equals(existingCategory.getCode())) {
long count = count(new LambdaQueryWrapper<ContentCategory>()
.eq(ContentCategory::getCode, category.getCode())
.ne(ContentCategory::getId, category.getId()));
if (count > 0) {
return Result.error("分类编码已存在");
}
}
// 设置更新时间
category.setUpdateTime(LocalDateTime.now());
// 更新分类
updateById(category);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteCategory(Long id) {
// 检查分类是否存在
ContentCategory category = getById(id);
if (category == null) {
return Result.error("分类不存在");
}
// 检查是否有子分类
long childCount = count(new LambdaQueryWrapper<ContentCategory>()
.eq(ContentCategory::getParentId, id));
if (childCount > 0) {
return Result.error("该分类下存在子分类,无法删除");
}
// 检查是否有关联的视频
Long videoCount = videoMapper.selectCount(new LambdaQueryWrapper<ContentVideo>()
.eq(ContentVideo::getCategoryId, id));
if (videoCount > 0) {
return Result.error("该分类下存在视频,无法删除");
}
// 删除分类
removeById(id);
return Result.success();
}
}
@@ -0,0 +1,122 @@
package com.nuaa.video.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.dto.DanmakuDTO;
import com.nuaa.video.content.entity.ContentDanmaku;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.mapper.ContentDanmakuMapper;
import com.nuaa.video.content.mapper.ContentVideoMapper;
import com.nuaa.video.content.mq.producer.VideoMessageProducer;
import com.nuaa.video.content.service.DanmakuService;
import com.nuaa.video.content.vo.DanmakuVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 弹幕服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DanmakuServiceImpl extends ServiceImpl<ContentDanmakuMapper, ContentDanmaku> implements DanmakuService {
private final ContentDanmakuMapper danmakuMapper;
private final ContentVideoMapper videoMapper;
private final VideoMessageProducer videoMessageProducer;
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Long> sendDanmaku(DanmakuDTO danmakuDTO, Long userId) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(danmakuDTO.getVideoId());
if (video == null) {
return Result.error("视频不存在");
}
// 创建弹幕对象
ContentDanmaku danmaku = new ContentDanmaku();
danmaku.setVideoId(danmakuDTO.getVideoId());
danmaku.setUserId(userId);
danmaku.setContent(danmakuDTO.getContent());
danmaku.setColor(danmakuDTO.getColor());
danmaku.setFontSize(danmakuDTO.getFontSize());
danmaku.setPosition(danmakuDTO.getPosition());
danmaku.setTime(danmakuDTO.getTime());
danmaku.setCreateTime(LocalDateTime.now());
// 保存弹幕
save(danmaku);
// 发送弹幕统计消息
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(danmakuDTO.getVideoId());
setUserId(userId);
setAction("danmaku");
setCount(1);
}}
);
return Result.success(danmaku.getId());
}
@Override
public Result<List<DanmakuVO>> getVideoTimeDanmaku(Long videoId, Float startTime, Float endTime) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 查询指定时间段的弹幕
List<ContentDanmaku> danmakus = danmakuMapper.getVideoTimeDanmaku(videoId, startTime, endTime);
// 转换为VO
List<DanmakuVO> danmakuVOList = danmakus.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return Result.success(danmakuVOList);
}
@Override
public Result<List<DanmakuVO>> getVideoAllDanmaku(Long videoId) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 查询所有弹幕
List<ContentDanmaku> danmakus = list(
new LambdaQueryWrapper<ContentDanmaku>()
.eq(ContentDanmaku::getVideoId, videoId)
.orderByAsc(ContentDanmaku::getTime)
);
// 转换为VO
List<DanmakuVO> danmakuVOList = danmakus.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return Result.success(danmakuVOList);
}
/**
* 将弹幕实体转换为VO
*/
private DanmakuVO convertToVO(ContentDanmaku danmaku) {
DanmakuVO vo = new DanmakuVO();
BeanUtils.copyProperties(danmaku, vo);
return vo;
}
}
@@ -0,0 +1,195 @@
package com.nuaa.video.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentUserFavorite;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.entity.ContentVideoFavorite;
import com.nuaa.video.content.mapper.ContentUserFavoriteMapper;
import com.nuaa.video.content.mapper.ContentVideoFavoriteMapper;
import com.nuaa.video.content.mapper.ContentVideoMapper;
import com.nuaa.video.content.service.FavoriteService;
import com.nuaa.video.content.service.VideoService;
import com.nuaa.video.content.vo.FavoriteVO;
import com.nuaa.video.content.vo.VideoListVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 收藏夹服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FavoriteServiceImpl extends ServiceImpl<ContentUserFavoriteMapper, ContentUserFavorite> implements FavoriteService {
private final ContentUserFavoriteMapper userFavoriteMapper;
private final ContentVideoFavoriteMapper videoFavoriteMapper;
private final ContentVideoMapper videoMapper;
private final VideoService videoService;
@Override
public Result<List<FavoriteVO>> getUserFavorites(Long userId) {
// 查询用户的收藏夹
List<ContentUserFavorite> favorites = list(
new LambdaQueryWrapper<ContentUserFavorite>()
.eq(ContentUserFavorite::getUserId, userId)
.orderByDesc(ContentUserFavorite::getUpdateTime)
);
// 转换为VO
List<FavoriteVO> favoriteVOList = favorites.stream()
.map(favorite -> {
FavoriteVO vo = new FavoriteVO();
BeanUtils.copyProperties(favorite, vo);
return vo;
})
.collect(Collectors.toList());
return Result.success(favoriteVOList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Long> createFavorite(ContentUserFavorite favorite, Long userId) {
// 设置默认值
favorite.setUserId(userId);
favorite.setVideoCount(0);
if (favorite.getIsPrivate() == null) {
favorite.setIsPrivate(0); // 默认公开
}
favorite.setCreateTime(LocalDateTime.now());
favorite.setUpdateTime(LocalDateTime.now());
// 保存收藏夹
save(favorite);
return Result.success(favorite.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateFavorite(ContentUserFavorite favorite, Long userId) {
// 检查收藏夹是否存在
ContentUserFavorite existingFavorite = getById(favorite.getId());
if (existingFavorite == null) {
return Result.error("收藏夹不存在");
}
// 检查收藏夹是否属于当前用户
if (!existingFavorite.getUserId().equals(userId)) {
return Result.error("无权操作该收藏夹");
}
// 设置不可修改的字段
favorite.setUserId(existingFavorite.getUserId());
favorite.setVideoCount(existingFavorite.getVideoCount());
favorite.setUpdateTime(LocalDateTime.now());
// 更新收藏夹
updateById(favorite);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteFavorite(Long id, Long userId) {
// 检查收藏夹是否存在
ContentUserFavorite favorite = getById(id);
if (favorite == null) {
return Result.error("收藏夹不存在");
}
// 检查收藏夹是否属于当前用户
if (!favorite.getUserId().equals(userId)) {
return Result.error("无权操作该收藏夹");
}
// 检查是否是默认收藏夹
if (favorite.getName().equals("默认收藏夹")) {
return Result.error("默认收藏夹不能删除");
}
// 删除收藏夹内的视频关联
videoFavoriteMapper.delete(
new LambdaQueryWrapper<ContentVideoFavorite>()
.eq(ContentVideoFavorite::getFavoriteId, id)
);
// 删除收藏夹
removeById(id);
return Result.success();
}
@Override
public Result<Page<VideoListVO>> getFavoriteVideos(Long favoriteId, Page<ContentVideo> page, Long userId) {
// 检查收藏夹是否存在
ContentUserFavorite favorite = getById(favoriteId);
if (favorite == null) {
return Result.error("收藏夹不存在");
}
// 检查是否有权限查看(收藏夹所有者或公开收藏夹)
if (favorite.getIsPrivate() == 1 && !favorite.getUserId().equals(userId)) {
return Result.error("无权查看该收藏夹");
}
// 查询收藏夹内的视频ID
List<Long> videoIds = videoFavoriteMapper.selectList(
new LambdaQueryWrapper<ContentVideoFavorite>()
.eq(ContentVideoFavorite::getFavoriteId, favoriteId)
.orderByDesc(ContentVideoFavorite::getCreateTime)
).stream().map(ContentVideoFavorite::getVideoId).collect(Collectors.toList());
if (videoIds.isEmpty()) {
Page<VideoListVO> emptyPage = new Page<>();
emptyPage.setRecords(new ArrayList<>());
emptyPage.setTotal(0);
emptyPage.setSize(page.getSize());
emptyPage.setCurrent(page.getCurrent());
emptyPage.setPages(0);
return Result.success(emptyPage);
}
// 查询视频信息
Page<ContentVideo> videoPage = videoMapper.selectPage(
page,
new LambdaQueryWrapper<ContentVideo>()
.in(ContentVideo::getId, videoIds)
.eq(ContentVideo::getStatus, 1) // 已发布
.orderByDesc(ContentVideo::getPublishTime)
);
// 转换为VideoListVO
List<VideoListVO> videoListVOs = new ArrayList<>();
for (ContentVideo video : videoPage.getRecords()) {
VideoListVO vo = new VideoListVO();
BeanUtils.copyProperties(video, vo);
// 可以通过VideoService获取更多信息,如标签、分类、用户信息等
videoListVOs.add(vo);
}
// 构建返回结果
Page<VideoListVO> resultPage = new Page<>();
resultPage.setRecords(videoListVOs);
resultPage.setTotal(videoPage.getTotal());
resultPage.setSize(videoPage.getSize());
resultPage.setCurrent(videoPage.getCurrent());
resultPage.setPages(videoPage.getPages());
return Result.success(resultPage);
}
}
@@ -0,0 +1,174 @@
package com.nuaa.video.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentTag;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.entity.ContentVideoTag;
import com.nuaa.video.content.mapper.ContentTagMapper;
import com.nuaa.video.content.mapper.ContentVideoMapper;
import com.nuaa.video.content.mapper.ContentVideoTagMapper;
import com.nuaa.video.content.service.TagService;
import com.nuaa.video.content.vo.TagVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 标签服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TagServiceImpl extends ServiceImpl<ContentTagMapper, ContentTag> implements TagService {
private final ContentTagMapper tagMapper;
private final ContentVideoTagMapper videoTagMapper;
private final ContentVideoMapper videoMapper;
@Override
public Result<List<TagVO>> getTagList() {
// 查询所有标签
List<ContentTag> tags = tagMapper.selectList(new LambdaQueryWrapper<>());
// 转换为VO
List<TagVO> tagVOList = tags.stream()
.map(tag -> {
TagVO tagVO = new TagVO();
BeanUtils.copyProperties(tag, tagVO);
return tagVO;
})
.collect(Collectors.toList());
return Result.success(tagVOList);
}
@Override
public Result<Page<TagVO>> getTagPage(Page<ContentTag> page, String keyword) {
// 构建查询条件
LambdaQueryWrapper<ContentTag> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
queryWrapper.like(ContentTag::getName, keyword);
}
queryWrapper.orderByDesc(ContentTag::getCreateTime);
// 分页查询
Page<ContentTag> tagPage = tagMapper.selectPage(page, queryWrapper);
// 转换为VO
Page<TagVO> tagVOPage = new Page<>();
BeanUtils.copyProperties(tagPage, tagVOPage, "records");
List<TagVO> tagVOList = tagPage.getRecords().stream()
.map(tag -> {
TagVO tagVO = new TagVO();
BeanUtils.copyProperties(tag, tagVO);
return tagVO;
})
.collect(Collectors.toList());
tagVOPage.setRecords(tagVOList);
return Result.success(tagVOPage);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> addTag(ContentTag tag) {
// 检查标签名称是否已存在
long count = count(new LambdaQueryWrapper<ContentTag>()
.eq(ContentTag::getName, tag.getName()));
if (count > 0) {
return Result.error("标签名称已存在");
}
// 设置创建时间和更新时间
tag.setCreateTime(LocalDateTime.now());
tag.setUpdateTime(LocalDateTime.now());
// 保存标签
save(tag);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateTag(ContentTag tag) {
// 检查标签是否存在
ContentTag existingTag = getById(tag.getId());
if (existingTag == null) {
return Result.error("标签不存在");
}
// 检查标签名称是否已存在(排除自身)
long count = count(new LambdaQueryWrapper<ContentTag>()
.eq(ContentTag::getName, tag.getName())
.ne(ContentTag::getId, tag.getId()));
if (count > 0) {
return Result.error("标签名称已存在");
}
// 设置更新时间
tag.setUpdateTime(LocalDateTime.now());
// 更新标签
updateById(tag);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteTag(Long id) {
// 检查标签是否存在
ContentTag tag = getById(id);
if (tag == null) {
return Result.error("标签不存在");
}
// 检查是否有关联的视频
Long count = videoTagMapper.selectCount(new LambdaQueryWrapper<ContentVideoTag>()
.eq(ContentVideoTag::getTagId, id));
if (count > 0) {
return Result.error("该标签下存在视频,无法删除");
}
// 删除标签
removeById(id);
return Result.success();
}
@Override
public Result<List<TagVO>> getVideoTags(Long videoId) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 查询视频的标签
List<ContentTag> tags = tagMapper.getTagsByVideoId(videoId);
// 转换为VO
List<TagVO> tagVOList = tags.stream()
.map(tag -> {
TagVO tagVO = new TagVO();
BeanUtils.copyProperties(tag, tagVO);
return tagVO;
})
.collect(Collectors.toList());
return Result.success(tagVOList);
}
}
@@ -0,0 +1,729 @@
package com.nuaa.video.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.common.exception.BusinessException;
import com.nuaa.video.common.model.enums.ResultCode;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.common.utils.MinioUtils;
import com.nuaa.video.content.dto.*;
import com.nuaa.video.content.entity.*;
import com.nuaa.video.content.enums.VideoStatusEnum;
import com.nuaa.video.content.feign.UserFeignClient;
import com.nuaa.video.content.mapper.*;
import com.nuaa.video.content.mq.producer.VideoMessageProducer;
import com.nuaa.video.content.service.TagService;
import com.nuaa.video.content.service.VideoService;
import com.nuaa.video.content.utils.VideoProcessor;
import com.nuaa.video.content.vo.TagVO;
import com.nuaa.video.content.vo.UploadResultVO;
import com.nuaa.video.content.vo.VideoInfoVO;
import com.nuaa.video.content.vo.VideoListVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 视频服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVideo> implements VideoService {
private final ContentVideoMapper videoMapper;
private final ContentCategoryMapper categoryMapper;
private final ContentVideoTagMapper videoTagMapper;
private final ContentVideoLikeMapper videoLikeMapper;
private final ContentVideoCoinMapper videoCoinMapper;
private final ContentVideoFavoriteMapper videoFavoriteMapper;
private final ContentUserFavoriteMapper userFavoriteMapper;
private final ContentVideoPlayRecordMapper playRecordMapper;
private final ContentVideoReviewMapper videoReviewMapper;
private final TagService tagService;
private final VideoProcessor videoProcessor;
private final MinioUtils minioUtils;
private final UserFeignClient userFeignClient;
private final VideoMessageProducer videoMessageProducer;
@Override
public Result<UploadResultVO> uploadVideo(MultipartFile videoFile, Long userId) {
try {
// 校验文件类型
String contentType = videoFile.getContentType();
if (contentType == null || !contentType.startsWith("video/")) {
return Result.error("不支持的文件类型,请上传视频文件");
}
// 处理视频
VideoProcessor.VideoProcessResult processResult = videoProcessor.processAndUpload(videoFile);
// 构建上传结果
UploadResultVO resultVO = new UploadResultVO();
resultVO.setVideoUrl(processResult.getVideoUrl());
resultVO.setCoverUrl(processResult.getCoverUrl());
resultVO.setDuration(processResult.getDuration());
resultVO.setWidth(processResult.getWidth());
resultVO.setHeight(processResult.getHeight());
resultVO.setFormat(processResult.getFormat());
resultVO.setSize(processResult.getSize());
return Result.success(resultVO);
} catch (Exception e) {
log.error("视频上传处理失败", e);
return Result.error("视频上传处理失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Long> createVideo(VideoUploadDTO uploadDTO, Long userId) {
try {
// 检查分类是否存在
ContentCategory category = categoryMapper.selectById(uploadDTO.getCategoryId());
if (category == null) {
return Result.error("分类不存在");
}
// 创建视频记录
ContentVideo video = new ContentVideo();
video.setUserId(userId);
video.setTitle(uploadDTO.getTitle());
video.setDescription(uploadDTO.getDescription());
video.setCoverUrl(uploadDTO.getCoverUrl());
video.setVideoUrl(uploadDTO.getVideoUrl());
video.setDuration(uploadDTO.getDuration());
video.setWidth(uploadDTO.getWidth());
video.setHeight(uploadDTO.getHeight());
video.setFormat(uploadDTO.getFormat());
video.setSize(uploadDTO.getSize());
video.setCategoryId(uploadDTO.getCategoryId());
// 设置默认状态为审核中
video.setStatus(VideoStatusEnum.REVIEWING.getCode());
video.setLikeCount(0);
video.setCoinCount(0);
video.setFavoriteCount(0);
video.setCommentCount(0);
video.setViewCount(0L);
video.setShareCount(0);
video.setDanmakuCount(0);
video.setIsRecommend(0);
video.setIsTop(0);
video.setCreatedBy(userId.toString());
video.setUpdatedBy(userId.toString());
video.setCreateTime(LocalDateTime.now());
video.setUpdateTime(LocalDateTime.now());
// 保存视频记录
save(video);
Long videoId = video.getId();
// 如果有标签,添加标签关联
if (uploadDTO.getTagIds() != null && !uploadDTO.getTagIds().isEmpty()) {
videoTagMapper.batchInsert(videoId, uploadDTO.getTagIds(), userId.toString());
}
// 更新用户视频数量
userFeignClient.increaseVideoCount(userId, 1, "Bearer ");
// 发送视频处理消息
// videoMessageProducer.sendVideoProcessMessage(videoId);
return Result.success(videoId);
} catch (Exception e) {
log.error("创建视频失败", e);
throw new BusinessException("创建视频失败: " + e.getMessage());
}
}
@Override
public Result<VideoInfoVO> getVideoInfo(Long id, Long userId) {
// 查询视频信息
ContentVideo video = getById(id);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 转换为VO
VideoInfoVO videoInfoVO = new VideoInfoVO();
BeanUtils.copyProperties(video, videoInfoVO);
// 获取分类名称
ContentCategory category = categoryMapper.selectById(video.getCategoryId());
if (category != null) {
videoInfoVO.setCategoryName(category.getName());
}
// 获取标签列表
Result<List<TagVO>> tagResult = tagService.getVideoTags(id);
if (tagResult.getCode() == ResultCode.SUCCESS.getCode() && tagResult.getData() != null) {
List<TagVO> tags = tagResult.getData();
videoInfoVO.setTagIds(tags.stream().map(TagVO::getId).collect(Collectors.toList()));
videoInfoVO.setTagNames(tags.stream().map(TagVO::getName).collect(Collectors.toList()));
}
// 获取用户信息
try {
Result<Object> userResult = userFeignClient.getUserProfile(video.getUserId());
if (userResult.getCode() == ResultCode.SUCCESS.getCode() && userResult.getData() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> userData = (Map<String, Object>) userResult.getData();
videoInfoVO.setUserNickname((String) userData.get("nickname"));
videoInfoVO.setUserAvatar((String) userData.get("avatar"));
}
} catch (Exception e) {
log.error("获取用户信息失败", e);
}
// 如果当前用户已登录,判断是否已点赞、收藏、投币
if (userId != null) {
int likeCount = videoLikeMapper.checkUserLiked(userId, id);
videoInfoVO.setIsLiked(likeCount > 0);
int favoriteCount = videoFavoriteMapper.checkUserFavorited(userId, id);
videoInfoVO.setIsFavorited(favoriteCount > 0);
int coinCount = videoCoinMapper.getUserCoinCount(userId, id);
videoInfoVO.setIsCoined(coinCount > 0);
// 增加播放数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(id);
setUserId(userId);
setAction("play");
setCount(1);
}}
);
}
return Result.success(videoInfoVO);
}
@Override
public Result<Page<VideoListVO>> getVideoList(Page<ContentVideo> page, VideoQueryDTO queryDTO, Long userId) {
// 如果没有指定状态,默认查询已发布的视频
if (queryDTO.getStatus() == null) {
queryDTO.setStatus(VideoStatusEnum.PUBLISHED.getCode());
}
// 查询视频列表
Page<ContentVideo> videoPage = videoMapper.selectVideoList(page, queryDTO);
List<ContentVideo> records = videoPage.getRecords();
// 查询视频的标签
List<VideoListVO> videoListVOs = new ArrayList<>();
for (ContentVideo video : records) {
VideoListVO vo = new VideoListVO();
BeanUtils.copyProperties(video, vo);
// 设置分类名称
vo.setCategoryName(video.getCategoryName());
// 获取标签列表
Result<List<TagVO>> tagResult = tagService.getVideoTags(video.getId());
if (tagResult.getCode() == ResultCode.SUCCESS.getCode() && tagResult.getData() != null) {
List<TagVO> tags = tagResult.getData();
vo.setTagNames(tags.stream().map(TagVO::getName).collect(Collectors.toList()));
}
// 获取用户信息
try {
Result<Object> userResult = userFeignClient.getUserProfile(video.getUserId());
if (userResult.getCode() == ResultCode.SUCCESS.getCode() && userResult.getData() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> userData = (Map<String, Object>) userResult.getData();
vo.setUserNickname((String) userData.get("nickname"));
vo.setUserAvatar((String) userData.get("avatar"));
}
} catch (Exception e) {
log.error("获取用户信息失败", e);
}
videoListVOs.add(vo);
}
// 构建返回结果
Page<VideoListVO> resultPage = new Page<>();
resultPage.setRecords(videoListVOs);
resultPage.setTotal(videoPage.getTotal());
resultPage.setSize(videoPage.getSize());
resultPage.setCurrent(videoPage.getCurrent());
resultPage.setPages(videoPage.getPages());
return Result.success(resultPage);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateVideo(VideoUpdateDTO updateDTO, Long userId) {
// 查询视频是否存在
ContentVideo video = getById(updateDTO.getId());
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 检查视频是否属于当前用户
if (!video.getUserId().equals(userId)) {
return Result.error(ResultCode.FORBIDDEN.getCode(), "无权修改该视频");
}
// 检查分类是否存在
if (updateDTO.getCategoryId() != null) {
ContentCategory category = categoryMapper.selectById(updateDTO.getCategoryId());
if (category == null) {
return Result.error("分类不存在");
}
video.setCategoryId(updateDTO.getCategoryId());
}
// 更新视频信息
if (updateDTO.getTitle() != null) {
video.setTitle(updateDTO.getTitle());
}
if (updateDTO.getDescription() != null) {
video.setDescription(updateDTO.getDescription());
}
if (updateDTO.getCoverUrl() != null) {
video.setCoverUrl(updateDTO.getCoverUrl());
}
video.setUpdatedBy(userId.toString());
video.setUpdateTime(LocalDateTime.now());
updateById(video);
// 更新标签关联
if (updateDTO.getTagIds() != null) {
// 删除原有标签关联
videoTagMapper.delete(
new LambdaQueryWrapper<ContentVideoTag>()
.eq(ContentVideoTag::getVideoId, video.getId())
);
// 添加新标签关联
if (!updateDTO.getTagIds().isEmpty()) {
videoTagMapper.batchInsert(video.getId(), updateDTO.getTagIds(), userId.toString());
}
}
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteVideo(Long id, Long userId) {
// 查询视频是否存在
ContentVideo video = getById(id);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 检查视频是否属于当前用户
if (!video.getUserId().equals(userId)) {
return Result.error(ResultCode.FORBIDDEN.getCode(), "无权删除该视频");
}
// 删除视频
removeById(id);
// 删除相关记录
// 1. 删除标签关联
videoTagMapper.delete(
new LambdaQueryWrapper<ContentVideoTag>()
.eq(ContentVideoTag::getVideoId, id)
);
// 2. 删除点赞记录
videoLikeMapper.delete(
new LambdaQueryWrapper<ContentVideoLike>()
.eq(ContentVideoLike::getVideoId, id)
);
// 3. 删除投币记录
videoCoinMapper.delete(
new LambdaQueryWrapper<ContentVideoCoin>()
.eq(ContentVideoCoin::getVideoId, id)
);
// 4. 删除收藏记录
videoFavoriteMapper.delete(
new LambdaQueryWrapper<ContentVideoFavorite>()
.eq(ContentVideoFavorite::getVideoId, id)
);
// 5. 删除播放记录
playRecordMapper.delete(
new LambdaQueryWrapper<ContentVideoPlayRecord>()
.eq(ContentVideoPlayRecord::getVideoId, id)
);
// 更新用户视频数量
userFeignClient.increaseVideoCount(userId, -1, "Bearer ");
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> likeVideo(Long videoId, Long userId) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 检查是否已点赞
int count = videoLikeMapper.checkUserLiked(userId, videoId);
if (count > 0) {
return Result.error("已经点赞过该视频");
}
// 添加点赞记录
ContentVideoLike like = new ContentVideoLike();
like.setVideoId(videoId);
like.setUserId(userId);
like.setCreateTime(LocalDateTime.now());
videoLikeMapper.insert(like);
// 发送统计消息,增加点赞数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("like");
setCount(1);
}}
);
// 更新用户获赞数
userFeignClient.increaseLikeCount(video.getUserId(), 1, "Bearer ");
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> unlikeVideo(Long videoId, Long userId) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 检查是否已点赞
int count = videoLikeMapper.checkUserLiked(userId, videoId);
if (count == 0) {
return Result.error("未点赞该视频");
}
// 删除点赞记录
videoLikeMapper.delete(
new LambdaQueryWrapper<ContentVideoLike>()
.eq(ContentVideoLike::getVideoId, videoId)
.eq(ContentVideoLike::getUserId, userId)
);
// 发送统计消息,减少点赞数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("like");
setCount(-1);
}}
);
// 更新用户获赞数
userFeignClient.increaseLikeCount(video.getUserId(), -1, "Bearer ");
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> coinVideo(Long videoId, Long userId, Integer amount) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 检查投币数量
if (amount == null || amount <= 0 || amount > 2) {
return Result.error("投币数量无效,请投1-2个硬币");
}
// 检查是否已投币
int coinCount = videoCoinMapper.getUserCoinCount(userId, videoId);
if (coinCount >= 2) {
return Result.error("已经投了2个硬币,不能再投了");
} else if (coinCount + amount > 2) {
return Result.error("投币总数不能超过2个");
}
// 添加投币记录
ContentVideoCoin coin = new ContentVideoCoin();
coin.setVideoId(videoId);
coin.setUserId(userId);
coin.setAmount(amount);
coin.setCreateTime(LocalDateTime.now());
videoCoinMapper.insert(coin);
// 发送统计消息,增加投币数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("coin");
setCount(amount);
}}
);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> favoriteVideo(FavoriteDTO favoriteDTO, Long userId) {
Long videoId = favoriteDTO.getVideoId();
Long favoriteId = favoriteDTO.getFavoriteId();
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 查询收藏夹是否存在
ContentUserFavorite favorite = userFavoriteMapper.selectById(favoriteId);
if (favorite == null) {
return Result.error("收藏夹不存在");
}
// 检查收藏夹是否属于当前用户
if (!favorite.getUserId().equals(userId)) {
return Result.error(ResultCode.FORBIDDEN.getCode(), "无权操作该收藏夹");
}
// 检查是否已收藏到该收藏夹
Long count = videoFavoriteMapper.selectCount(
new LambdaQueryWrapper<ContentVideoFavorite>()
.eq(ContentVideoFavorite::getVideoId, videoId)
.eq(ContentVideoFavorite::getUserId, userId)
.eq(ContentVideoFavorite::getFavoriteId, favoriteId)
);
if (count > 0) {
return Result.error("已经收藏到该收藏夹");
}
// 添加收藏记录
ContentVideoFavorite videoFavorite = new ContentVideoFavorite();
videoFavorite.setVideoId(videoId);
videoFavorite.setUserId(userId);
videoFavorite.setFavoriteId(favoriteId);
videoFavorite.setCreateTime(LocalDateTime.now());
videoFavoriteMapper.insert(videoFavorite);
// 增加收藏夹视频数量
userFavoriteMapper.increaseVideoCount(favoriteId, 1);
// 发送统计消息,增加收藏数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("favorite");
setCount(1);
}}
);
// 更新用户收藏数
userFeignClient.increaseFavoriteCount(userId, 1, "Bearer ");
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> unfavoriteVideo(Long videoId, Long userId) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 查询是否已收藏
List<ContentVideoFavorite> favorites = videoFavoriteMapper.selectList(
new LambdaQueryWrapper<ContentVideoFavorite>()
.eq(ContentVideoFavorite::getVideoId, videoId)
.eq(ContentVideoFavorite::getUserId, userId)
);
if (favorites.isEmpty()) {
return Result.error("未收藏该视频");
}
// 删除收藏记录并更新收藏夹视频数量
for (ContentVideoFavorite favorite : favorites) {
videoFavoriteMapper.deleteById(favorite.getId());
userFavoriteMapper.increaseVideoCount(favorite.getFavoriteId(), -1);
}
// 发送统计消息,减少收藏数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("favorite");
setCount(-favorites.size());
}}
);
// 更新用户收藏数
userFeignClient.increaseFavoriteCount(userId, -favorites.size(), "Bearer ");
return Result.success();
}
@Override
public Result<Void> shareVideo(Long videoId, Long userId, String platform) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 发送统计消息,增加分享数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("share");
setCount(1);
}}
);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> playVideo(PlayRecordDTO recordDTO, Long userId) {
Long videoId = recordDTO.getVideoId();
Integer playTime = recordDTO.getPlayTime();
Integer isFinished = recordDTO.getIsFinished();
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 如果isFinished未设置,根据播放时长判断是否看完
if (isFinished == null) {
isFinished = (playTime >= video.getDuration() * 0.9) ? 1 : 0;
}
// 查询是否有播放记录
ContentVideoPlayRecord record = playRecordMapper.selectOne(
new LambdaQueryWrapper<ContentVideoPlayRecord>()
.eq(ContentVideoPlayRecord::getVideoId, videoId)
.eq(ContentVideoPlayRecord::getUserId, userId)
);
if (record == null) {
// 添加播放记录
record = new ContentVideoPlayRecord();
record.setVideoId(videoId);
record.setUserId(userId);
record.setPlayTime(playTime);
record.setIsFinished(isFinished);
record.setCreateTime(LocalDateTime.now());
record.setUpdateTime(LocalDateTime.now());
playRecordMapper.insert(record);
// 首次播放,增加播放数
videoMessageProducer.sendVideoStatsMessage(
new VideoMessageProducer.VideoStatsMessage() {{
setVideoId(videoId);
setUserId(userId);
setAction("play");
setCount(1);
}}
);
} else {
// 更新播放记录
record.setPlayTime(Math.max(record.getPlayTime(), playTime));
if (isFinished == 1) {
record.setIsFinished(1);
}
record.setUpdateTime(LocalDateTime.now());
playRecordMapper.updateById(record);
}
return Result.success();
}
@Override
public Result<Page<VideoListVO>> getUserVideos(Long userId, Page<ContentVideo> page, Long currentUserId) {
VideoQueryDTO queryDTO = new VideoQueryDTO();
queryDTO.setUserId(userId);
// 如果是查询自己的视频,显示所有状态的视频
// 如果是查询他人的视频,只显示已发布的视频
if (currentUserId != null && currentUserId.equals(userId)) {
queryDTO.setStatus(null);
} else {
queryDTO.setStatus(VideoStatusEnum.PUBLISHED.getCode());
}
return getVideoList(page, queryDTO, currentUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> reviewVideo(Long videoId, Long reviewerId, Integer status, String reason) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
}
// 检查状态是否合法
if (status != 1 && status != 3) {
return Result.error("审核状态无效,只能为通过(1)或不通过(3)");
}
// 更新视频状态
video.setStatus(status);
if (status == 1) {
// 通过审核,设置发布时间
video.setPublishTime(LocalDateTime.now());
}
video.setUpdatedBy(reviewerId.toString());
video.setUpdateTime(LocalDateTime.now());
updateById(video);
// 添加审核记录
ContentVideoReview review = new ContentVideoReview();
review.setVideoId(videoId);
review.setReviewerId(reviewerId);
review.setStatus(status);
review.setReason(reason);
review.setCreatedBy(reviewerId.toString());
review.setCreateTime(LocalDateTime.now());
videoReviewMapper.insert(review);
return Result.success();
}
}
@@ -0,0 +1,127 @@
package com.nuaa.video.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.entity.ContentVideoPlayRecord;
import com.nuaa.video.content.entity.ContentVideoStatDaily;
import com.nuaa.video.content.mapper.ContentVideoMapper;
import com.nuaa.video.content.mapper.ContentVideoPlayRecordMapper;
import com.nuaa.video.content.mapper.ContentVideoStatDailyMapper;
import com.nuaa.video.content.service.VideoStatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 视频统计服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VideoStatServiceImpl implements VideoStatService {
private final ContentVideoStatDailyMapper statDailyMapper;
private final ContentVideoMapper videoMapper;
private final ContentVideoPlayRecordMapper playRecordMapper;
@Override
public Result<List<ContentVideoStatDaily>> getVideoStatDaily(Long videoId, LocalDate startDate, LocalDate endDate) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 查询指定日期范围的统计数据
List<ContentVideoStatDaily> statList = statDailyMapper.selectList(
new LambdaQueryWrapper<ContentVideoStatDaily>()
.eq(ContentVideoStatDaily::getVideoId, videoId)
.between(ContentVideoStatDaily::getStatDate, startDate, endDate)
.orderByAsc(ContentVideoStatDaily::getStatDate)
);
return Result.success(statList);
}
@Override
public Result<Map<Integer, Long>> getVideoPlayTrend(Long videoId, LocalDate date) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 初始化24小时数据
Map<Integer, Long> hourlyPlays = new HashMap<>();
for (int i = 0; i < 24; i++) {
hourlyPlays.put(i, 0L);
}
// 查询当日播放记录
List<ContentVideoPlayRecord> records = playRecordMapper.selectList(
new LambdaQueryWrapper<ContentVideoPlayRecord>()
.eq(ContentVideoPlayRecord::getVideoId, videoId)
.apply("DATE(create_time) = {0}", date)
);
// 统计每小时播放量
records.forEach(record -> {
int hour = record.getCreateTime().getHour();
hourlyPlays.put(hour, hourlyPlays.getOrDefault(hour, 0L) + 1);
});
return Result.success(hourlyPlays);
}
@Override
public Result<Map<String, Object>> getVideoInteractionStats(Long videoId) {
// 检查视频是否存在
ContentVideo video = videoMapper.selectById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 获取视频统计数据
Map<String, Object> stats = new HashMap<>();
stats.put("viewCount", video.getViewCount());
stats.put("likeCount", video.getLikeCount());
stats.put("favoriteCount", video.getFavoriteCount());
stats.put("coinCount", video.getCoinCount());
stats.put("commentCount", video.getCommentCount());
stats.put("shareCount", video.getShareCount());
stats.put("danmakuCount", video.getDanmakuCount());
// 计算互动率
if (video.getViewCount() > 0) {
double likeRate = (double) video.getLikeCount() / video.getViewCount() * 100;
double favoriteRate = (double) video.getFavoriteCount() / video.getViewCount() * 100;
double coinRate = (double) video.getCoinCount() / video.getViewCount() * 100;
double commentRate = (double) video.getCommentCount() / video.getViewCount() * 100;
stats.put("likeRate", String.format("%.2f", likeRate));
stats.put("favoriteRate", String.format("%.2f", favoriteRate));
stats.put("coinRate", String.format("%.2f", coinRate));
stats.put("commentRate", String.format("%.2f", commentRate));
}
// 获取最近30天的统计数据
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusDays(29);
List<ContentVideoStatDaily> dailyStats = statDailyMapper.selectList(
new LambdaQueryWrapper<ContentVideoStatDaily>()
.eq(ContentVideoStatDaily::getVideoId, videoId)
.between(ContentVideoStatDaily::getStatDate, startDate, endDate)
.orderByAsc(ContentVideoStatDaily::getStatDate)
);
stats.put("dailyStats", dailyStats);
return Result.success(stats);
}
}
@@ -0,0 +1,63 @@
package com.nuaa.video.content.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nuaa.video.content.entity.ContentVideoStatDaily;
import com.nuaa.video.content.mapper.ContentVideoStatDailyMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
/**
* 视频统计数据同步定时任务
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class VideoStatsSyncTask {
private final ContentVideoStatDailyMapper statDailyMapper;
/**
* 每日凌晨0点30分执行,初始化当天的统计记录
*/
@Scheduled(cron = "0 30 0 * * ?")
@Transactional(rollbackFor = Exception.class)
public void initDailyStats() {
log.info("开始初始化今日视频统计记录");
try {
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
// 查询昨天有数据更新的视频ID
// 实际业务中可能会从Redis等缓存中获取活跃视频ID
// 这里简化处理,仅初始化部分必要的视频
log.info("今日视频统计记录初始化完成");
} catch (Exception e) {
log.error("初始化今日视频统计记录失败", e);
}
}
/**
* 每日凌晨1点执行,汇总前一天的数据
*/
@Scheduled(cron = "0 0 1 * * ?")
@Transactional(rollbackFor = Exception.class)
public void summarizeYesterdayStats() {
log.info("开始汇总昨日视频统计数据");
try {
LocalDate yesterday = LocalDate.now().minusDays(1);
// 从缓存中获取昨日视频互动统计数据,并更新到数据库
// 实际业务中可能会有更复杂的逻辑
log.info("昨日视频统计数据汇总完成");
} catch (Exception e) {
log.error("汇总昨日视频统计数据失败", e);
}
}
}
@@ -0,0 +1,116 @@
package com.nuaa.video.content.utils;
import com.nuaa.video.common.utils.MinioUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.probe.FFmpegFormat;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.probe.FFmpegStream;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
/**
* 视频处理工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class VideoProcessor {
private final FFmpeg ffmpeg;
private final FFprobe ffprobe;
private final MinioUtils minioUtils;
/**
* 处理视频并上传
*
* @param videoFile 视频文件
* @return 处理结果
* @throws IOException IO异常
*/
public VideoProcessResult processAndUpload(MultipartFile videoFile) throws Exception {
// 创建临时文件
Path tempDir = Files.createTempDirectory("video_upload_");
File tempFile = new File(tempDir.toFile(), UUID.randomUUID() + "_" + videoFile.getOriginalFilename());
videoFile.transferTo(tempFile);
// 分析视频信息
FFmpegProbeResult probeResult = ffprobe.probe(tempFile.getAbsolutePath());
FFmpegFormat format = probeResult.getFormat();
FFmpegStream videoStream = probeResult.getStreams().get(0);
// 视频信息
int width = videoStream.width;
int height = videoStream.height;
double duration = format.duration;
String codecName = videoStream.codec_name;
long size = tempFile.length();
// 生成缩略图
File thumbnailFile = new File(tempDir.toFile(), UUID.randomUUID() + "_thumbnail.jpg");
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(tempFile.getAbsolutePath())
.overrideOutputFiles(true)
.addOutput(thumbnailFile.getAbsolutePath())
.setFrames(1)
.setVideoFilter("select=eq(n\\,0)")
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
// 上传视频和缩略图到MinIO
String videoUrl = minioUtils.uploadObject("videos", tempFile.getName(),
new FileInputStream(tempFile), tempFile.length(), videoFile.getContentType());
String thumbnailUrl = minioUtils.uploadObject("covers", thumbnailFile.getName(),
new FileInputStream(thumbnailFile), thumbnailFile.length(), "image/jpeg");
// 构建结果
VideoProcessResult result = new VideoProcessResult();
result.setVideoUrl(videoUrl);
result.setCoverUrl(thumbnailUrl);
result.setDuration((int) Math.ceil(duration));
result.setWidth(width);
result.setHeight(height);
result.setFormat(codecName);
result.setSize(size);
// 清理临时文件
try {
Files.deleteIfExists(tempFile.toPath());
Files.deleteIfExists(thumbnailFile.toPath());
Files.deleteIfExists(tempDir);
} catch (IOException e) {
log.warn("清理临时文件失败", e);
}
return result;
}
/**
* 视频处理结果类
*/
@lombok.Data
public static class VideoProcessResult {
private String videoUrl;
private String coverUrl;
private int duration;
private int width;
private int height;
private String format;
private long size;
}
}
@@ -0,0 +1,54 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分类VO
*/
@Data
public class CategoryVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分类ID
*/
private Long id;
/**
* 分类名称
*/
private String name;
/**
* 分类编码
*/
private String code;
/**
* 父分类ID
*/
private Long parentId;
/**
* 分类层级
*/
private Integer level;
/**
* 排序
*/
private Integer sort;
/**
* 图标
*/
private String icon;
/**
* 子分类列表
*/
private List<CategoryVO> children;
}
@@ -0,0 +1,54 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 弹幕VO
*/
@Data
public class DanmakuVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 弹幕ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 弹幕内容
*/
private String content;
/**
* 弹幕颜色
*/
private String color;
/**
* 字体大小
*/
private Integer fontSize;
/**
* 位置(0:滚动,1:顶部,2:底部)
*/
private Integer position;
/**
* 发送时间(秒)
*/
private Float time;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,54 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 收藏夹VO
*/
@Data
public class FavoriteVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 收藏夹ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 收藏夹名称
*/
private String name;
/**
* 收藏夹描述
*/
private String description;
/**
* 是否私有(0:公开,1:私有)
*/
private Integer isPrivate;
/**
* 视频数量
*/
private Integer videoCount;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,23 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 标签VO
*/
@Data
public class TagVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 标签ID
*/
private Long id;
/**
* 标签名称
*/
private String name;
}
@@ -0,0 +1,53 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 上传结果VO
*/
@Data
public class UploadResultVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
private Long videoId;
/**
* 视频URL
*/
private String videoUrl;
/**
* 封面URL
*/
private String coverUrl;
/**
* 视频时长(秒)
*/
private Integer duration;
/**
* 视频宽度
*/
private Integer width;
/**
* 视频高度
*/
private Integer height;
/**
* 视频格式
*/
private String format;
/**
* 视频大小(字节)
*/
private Long size;
}
@@ -0,0 +1,145 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 视频信息VO
*/
@Data
public class VideoInfoVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 用户昵称
*/
private String userNickname;
/**
* 用户头像
*/
private String userAvatar;
/**
* 视频标题
*/
private String title;
/**
* 视频描述
*/
private String description;
/**
* 封面URL
*/
private String coverUrl;
/**
* 视频URL
*/
private String videoUrl;
/**
* 视频时长(秒)
*/
private Integer duration;
/**
* 视频宽度
*/
private Integer width;
/**
* 视频高度
*/
private Integer height;
/**
* 分类ID
*/
private Long categoryId;
/**
* 分类名称
*/
private String categoryName;
/**
* 标签ID列表
*/
private List<Long> tagIds;
/**
* 标签名称列表
*/
private List<String> tagNames;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 投币数
*/
private Integer coinCount;
/**
* 收藏数
*/
private Integer favoriteCount;
/**
* 评论数
*/
private Integer commentCount;
/**
* 播放数
*/
private Long viewCount;
/**
* 分享数
*/
private Integer shareCount;
/**
* 弹幕数
*/
private Integer danmakuCount;
/**
* 是否已点赞
*/
private Boolean isLiked;
/**
* 是否已收藏
*/
private Boolean isFavorited;
/**
* 是否已投币
*/
private Boolean isCoined;
/**
* 发布时间
*/
private LocalDateTime publishTime;
}
@@ -0,0 +1,85 @@
package com.nuaa.video.content.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 视频列表VO
*/
@Data
public class VideoListVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 用户昵称
*/
private String userNickname;
/**
* 用户头像
*/
private String userAvatar;
/**
* 视频标题
*/
private String title;
/**
* 视频描述
*/
private String description;
/**
* 封面URL
*/
private String coverUrl;
/**
* 视频时长(秒)
*/
private Integer duration;
/**
* 分类名称
*/
private String categoryName;
/**
* 标签名称列表
*/
private List<String> tagNames;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 播放数
*/
private Long viewCount;
/**
* 评论数
*/
private Integer commentCount;
/**
* 发布时间
*/
private LocalDateTime publishTime;
}
@@ -0,0 +1,42 @@
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://5.180.253.115:3306/video_content?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: sty@G458221
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
database: 2
host: 5.180.253.115
port: 6379
password: redis_EsJ8FX
timeout: 10000
rocketmq:
name-server: 5.180.253.115:9876
minio:
endpoint: http://5.180.253.115:9002
accessKey: minioadmin
secretKey: minioadmin
bucketName: video-platform
# 开发环境使用相对路径或本地FFmpeg
ffmpeg:
binary:
path: D:/ffmpeg/bin/ffmpeg.exe # Windows路径示例,实际环境请修改
probe:
path: D:/ffmpeg/bin/ffprobe.exe # Windows路径示例,实际环境请修改
temp:
path: D:/temp/ffmpeg # 临时文件目录
logging:
level:
com.nuaa.video: debug
org.springframework.security: debug
+112 -18
View File
@@ -4,40 +4,134 @@ server:
spring:
application:
name: video-content
profiles:
active: dev
config:
import:
- optional:nacos:${spring.application.name}.yaml
cloud:
nacos:
discovery:
server-addr: 5.180.253.115:8848
namespace: public
group: DEFAULT_GROUP
server-addr: ${NACOS_HOST:5.180.253.115}:${NACOS_PORT:8848}
namespace: ${NACOS_NAMESPACE:public}
group: ${NACOS_GROUP:DEFAULT_GROUP}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
sentinel:
transport:
dashboard: 5.180.253.115:8080
dashboard: ${SENTINEL_DASHBOARD_HOST:5.180.253.115}:${SENTINEL_DASHBOARD_PORT:8080}
port: 8722
eager: true
datasource:
url: jdbc:mysql://5.180.253.115:3306/video_content?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
rocketmq:
name-server: 5.180.253.115:9876
producer:
group: video-content-group
url: jdbc:mysql://${MYSQL_HOST:5.180.253.115}:${MYSQL_PORT:3306}/video_content?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:sty@G458221}
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
max-lifetime: 1200000
connection-timeout: 30000
connection-test-query: SELECT 1
data:
redis:
host: 5.180.253.115
port: 6379
password:
host: ${REDIS_HOST:5.180.253.115}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:redis_EsJ8FX}
database: 2
timeout: 10000
lettuce:
pool:
max-active: 200
max-wait: -1
max-idle: 10
min-idle: 0
# 文件上传配置
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
enabled: true
# RocketMQ配置
rocketmq:
name-server: ${ROCKETMQ_HOST:5.180.253.115}:${ROCKETMQ_PORT:9876}
producer:
group: video-content-producer
send-message-timeout: 3000
retry-times-when-send-failed: 3
retry-times-when-send-async-failed: 3
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.nuaa.video.content.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
# MinIO配置
minio:
endpoint: http://5.180.253.115:9002
accessKey: minioadmin
secretKey: minioadmin
bucketName: video-platform
endpoint: http://${MINIO_HOST:5.180.253.115}:${MINIO_PORT:9002}
accessKey: ${MINIO_ACCESS_KEY:minioadmin}
secretKey: ${MINIO_SECRET_KEY:minioadmin}
bucketName: ${MINIO_BUCKET:video-platform}
# FFmpeg配置
ffmpeg:
binary:
path: ${FFMPEG_PATH:/usr/bin/ffmpeg}
probe:
path: ${FFPROBE_PATH:/usr/bin/ffprobe}
temp:
path: ${FFMPEG_TEMP_PATH:/tmp/ffmpeg}
auth:
ignore-urls:
- /content/video/public/**
- /content/category/list
- /content/tag/list
- /actuator/**
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
circuitbreaker:
enabled: true
# JWT配置与auth模块保持一致
jwt:
secret: video-platform-secret-key-2024-nuaa
expiration: 86400
token-header: Authorization
token-prefix: "Bearer "
logging:
level:
com.nuaa.video: info
org.springframework.security: info
@@ -0,0 +1,16 @@
<?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.content.mapper.ContentDanmakuMapper">
<!-- 获取视频指定时间段的弹幕 -->
<select id="getVideoTimeDanmaku" resultType="com.nuaa.video.content.entity.ContentDanmaku">
<![CDATA[
SELECT *
FROM content_danmaku
WHERE video_id = #{videoId}
AND time >= #{startTime}
AND time <= #{endTime}
ORDER BY time ASC
]]>
</select>
</mapper>
@@ -0,0 +1,18 @@
<?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.content.mapper.ContentTagMapper">
<!-- 获取视频的标签 -->
<select id="getTagsByVideoId" resultType="com.nuaa.video.content.entity.ContentTag">
SELECT
t.*
FROM
content_tag t
INNER JOIN
content_video_tag vt ON t.id = vt.tag_id
WHERE
vt.video_id = #{videoId}
ORDER BY
t.id
</select>
</mapper>
@@ -0,0 +1,11 @@
<?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.content.mapper.ContentUserFavoriteMapper">
<!-- 增加收藏夹视频数量 -->
<update id="increaseVideoCount">
UPDATE content_user_favorite
SET video_count = video_count + #{count}
WHERE id = #{favoriteId}
</update>
</mapper>
@@ -0,0 +1,15 @@
<?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.content.mapper.ContentVideoCoinMapper">
<!-- 获取用户对视频的投币数量 -->
<select id="getUserCoinCount" resultType="int">
SELECT
IFNULL(SUM(amount), 0)
FROM
content_video_coin
WHERE
user_id = #{userId}
AND video_id = #{videoId}
</select>
</mapper>
@@ -0,0 +1,15 @@
<?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.content.mapper.ContentVideoFavoriteMapper">
<!-- 检查用户是否已收藏视频 -->
<select id="checkUserFavorited" resultType="int">
SELECT
COUNT(1)
FROM
content_video_favorite
WHERE
user_id = #{userId}
AND video_id = #{videoId}
</select>
</mapper>
@@ -0,0 +1,15 @@
<?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.content.mapper.ContentVideoLikeMapper">
<!-- 检查用户是否已点赞视频 -->
<select id="checkUserLiked" resultType="int">
SELECT
COUNT(1)
FROM
content_video_like
WHERE
user_id = #{userId}
AND video_id = #{videoId}
</select>
</mapper>
@@ -0,0 +1,109 @@
<?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.content.mapper.ContentVideoMapper">
<!-- 分页查询视频列表 -->
<select id="selectVideoList" resultType="com.nuaa.video.content.entity.ContentVideo">
SELECT
v.*,
c.name as category_name
FROM
content_video v
LEFT JOIN
content_category c ON v.category_id = c.id
<where>
<if test="query.status != null">
AND v.status = #{query.status}
</if>
<if test="query.userId != null">
AND v.user_id = #{query.userId}
</if>
<if test="query.categoryId != null">
AND v.category_id = #{query.categoryId}
</if>
<if test="query.isRecommend != null">
AND v.is_recommend = #{query.isRecommend}
</if>
<if test="query.isTop != null">
AND v.is_top = #{query.isTop}
</if>
<if test="query.keyword != null and query.keyword != ''">
AND (v.title LIKE CONCAT('%', #{query.keyword}, '%')
OR v.description LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
<if test="query.tagIds != null and query.tagIds.size > 0">
AND v.id IN (
SELECT
DISTINCT vt.video_id
FROM
content_video_tag vt
WHERE
vt.tag_id IN
<foreach collection="query.tagIds" item="tagId" open="(" separator="," close=")">
#{tagId}
</foreach>
)
</if>
</where>
<choose>
<when test="query.orderBy != null and query.orderBy != ''">
ORDER BY v.${query.orderBy}
<if test="query.orderDirection != null and query.orderDirection != ''">
${query.orderDirection}
</if>
</when>
<otherwise>
ORDER BY v.is_top DESC, v.publish_time DESC
</otherwise>
</choose>
</select>
<!-- 增加点赞数 -->
<update id="increaseLikeCount">
UPDATE content_video
SET like_count = like_count + #{count}
WHERE id = #{videoId}
</update>
<!-- 增加投币数 -->
<update id="increaseCoinCount">
UPDATE content_video
SET coin_count = coin_count + #{count}
WHERE id = #{videoId}
</update>
<!-- 增加收藏数 -->
<update id="increaseFavoriteCount">
UPDATE content_video
SET favorite_count = favorite_count + #{count}
WHERE id = #{videoId}
</update>
<!-- 增加评论数 -->
<update id="increaseCommentCount">
UPDATE content_video
SET comment_count = comment_count + #{count}
WHERE id = #{videoId}
</update>
<!-- 增加播放数 -->
<update id="increaseViewCount">
UPDATE content_video
SET view_count = view_count + #{count}
WHERE id = #{videoId}
</update>
<!-- 增加分享数 -->
<update id="increaseShareCount">
UPDATE content_video
SET share_count = share_count + #{count}
WHERE id = #{videoId}
</update>
<!-- 增加弹幕数 -->
<update id="increaseDanmakuCount">
UPDATE content_video
SET danmaku_count = danmaku_count + #{count}
WHERE id = #{videoId}
</update>
</mapper>
@@ -0,0 +1,29 @@
<?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.content.mapper.ContentVideoPlayRecordMapper">
<!-- 获取用户播放过的视频ID列表 -->
<select id="selectUserPlayedVideoIds" resultType="java.lang.Long">
SELECT video_id
FROM content_video_play_record
WHERE user_id = #{userId}
ORDER BY update_time DESC
LIMIT #{limit}
</select>
<!-- 统计按小时的播放量 -->
<select id="countHourlyPlays" resultType="java.util.Map">
SELECT
HOUR(create_time) as hour,
COUNT(1) as count
FROM
content_video_play_record
WHERE
video_id = #{videoId}
AND DATE(create_time) = #{date}
GROUP BY
HOUR(create_time)
ORDER BY
hour
</select>
</mapper>
@@ -0,0 +1,24 @@
<?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.content.mapper.ContentVideoReviewMapper">
<!-- 查询视频最新审核记录 -->
<select id="selectLatestReview" resultType="com.nuaa.video.content.entity.ContentVideoReview">
SELECT *
FROM content_video_review
WHERE video_id = #{videoId}
ORDER BY create_time DESC
LIMIT 1
</select>
<!-- 根据审核状态查询视频审核记录 -->
<select id="selectReviewsByStatus" resultType="com.nuaa.video.content.entity.ContentVideoReview">
SELECT *
FROM content_video_review
WHERE status = #{status}
<if test="reviewerId != null">
AND reviewer_id = #{reviewerId}
</if>
ORDER BY create_time DESC
</select>
</mapper>
@@ -0,0 +1,29 @@
<?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.content.mapper.ContentVideoStatDailyMapper">
<!-- 获取视频日期范围内的统计数据 -->
<select id="selectStatsByDateRange" resultType="com.nuaa.video.content.entity.ContentVideoStatDaily">
SELECT *
FROM content_video_stat_daily
WHERE video_id = #{videoId}
AND stat_date BETWEEN #{startDate} AND #{endDate}
ORDER BY stat_date ASC
</select>
<!-- 获取视频总互动统计 -->
<select id="selectVideoInteractionStats" resultType="java.util.Map">
SELECT
SUM(like_increment) as totalLikes,
SUM(coin_increment) as totalCoins,
SUM(favorite_increment) as totalFavorites,
SUM(comment_increment) as totalComments,
SUM(view_increment) as totalViews,
SUM(share_increment) as totalShares,
SUM(danmaku_increment) as totalDanmakus
FROM
content_video_stat_daily
WHERE
video_id = #{videoId}
</select>
</mapper>
@@ -0,0 +1,13 @@
<?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.content.mapper.ContentVideoTagMapper">
<!-- 批量插入视频标签关联 -->
<insert id="batchInsert">
INSERT INTO content_video_tag (video_id, tag_id, created_by, create_time)
VALUES
<foreach collection="tagIds" item="tagId" separator=",">
(#{videoId}, #{tagId}, #{createdBy}, NOW())
</foreach>
</insert>
</mapper>