一、业务服务接口层(Service)
本文档包含视频管理功能的完整代码实现、功能描述以及 Postman API 测试指南。
一、业务服务接口层(Service)
1.1 VideoSerivice - 视频服务接口
路径:com.netflix.clone.demo.service.VideoSerivice
功能:定义视频管理相关的业务方法契约,包括管理员 CRUD 操作和普通用户浏览视频的接口。
package com.netflix.clone.demo.service;
import com.netflix.clone.demo.dto.request.VideoRequest;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.dto.response.PageResponse;
import com.netflix.clone.demo.dto.response.VideoResponse;
import com.netflix.clone.demo.dto.response.VideoStatsResponse;
import jakarta.validation.Valid;
import java.util.List;
public interface VideoSerivice {
MessageResponse createVideoByAdmin(@Valid VideoRequest videoRequest);
PageResponse<VideoResponse> getAllAdminVideos(int page, int size, String search);
MessageResponse updateVideoAdmin(Long id, @Valid VideoRequest videoRequest);
MessageResponse deleteVideoByAdmin(Long id);
MessageResponse toggleVideoPublishStatusByAdmin(Long id, boolean value);
VideoStatsResponse getAdminStats();
PageResponse<VideoResponse> getPublishedVideos(int page, int size, String search, String email);
List<VideoResponse> getFeaturedVideos();
}
⚠️ 拼写错误提醒(仅指出,不修改):
- 接口名
VideoSerivice应为VideoService(Service拼写错误)。- 返回类型
VideoStatsResponse在方法getAdminStats中使用,但实际存在的 DTO 是VideoStatusResponse,需确保命名一致。
二、业务服务实现层(Service.Impl)
2.1 VideoServiceImpl - 视频服务实现类
路径:com.netflix.clone.demo.service.Impl.VideoServiceImpl
功能:实现 VideoSerivice 接口,处理视频的增删改查、发布状态切换、统计信息获取以及普通用户的视频浏览(支持搜索和 watchlist 状态标记)。
package com.netflix.clone.demo.service.Impl;
import com.netflix.clone.demo.dao.UserRepository;
import com.netflix.clone.demo.dao.VideoRepository;
import com.netflix.clone.demo.dto.request.VideoRequest;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.dto.response.PageResponse;
import com.netflix.clone.demo.dto.response.VideoResponse;
import com.netflix.clone.demo.dto.response.VideoStatsResponse;
import com.netflix.clone.demo.entity.Video;
import com.netflix.clone.demo.service.VideoSerivice;
import com.netflix.clone.demo.util.PaginationUtils;
import com.netflix.clone.demo.util.ServiceUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
@Service
public class VideoServiceImpl implements VideoSerivice {
@Autowired
private VideoRepository videoRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private ServiceUtils serviceUtils;
/**
* 管理员创建新视频
*/
@Override
public MessageResponse createVideoByAdmin(VideoRequest videoRequest) {
Video video = new Video();
video.setTitle(videoRequest.getTitle());
video.setDescription(videoRequest.getDescription());
video.setYear(videoRequest.getYear());
video.setRating(videoRequest.getRating());
video.setDuration(videoRequest.getDuration());
video.setSrcUuid(videoRequest.getSrc());
video.setPosterUuid(videoRequest.getPoster());
video.setPublished(videoRequest.isPublished());
video.setCategories(videoRequest.getCategories() != null ? videoRequest.getCategories() :
List.of());
videoRepository.save(video);
return new MessageResponse("Video created successfully.");
}
/**
* 管理员分页获取所有视频(支持搜索)
*/
@Override
public PageResponse<VideoResponse> getAllAdminVideos(int page, int size, String search) {
Pageable pageable = PaginationUtils.createPageRequest(page, size, "id");
Page<Video> videoPage;
if (search != null && !search.trim().isEmpty()) {
videoPage = videoRepository.searchVideos(search.trim(), pageable);
} else {
videoPage = videoRepository.findAll(pageable);
}
return PaginationUtils.toPageResponse(videoPage, VideoResponse::fromEntity);
}
/**
* 管理员更新视频信息
*/
@Override
public MessageResponse updateVideoAdmin(Long id, VideoRequest videoRequest) {
Video video = new Video();
video.setId(id);
video.setTitle(videoRequest.getTitle());
video.setDescription(videoRequest.getDescription());
video.setYear(videoRequest.getYear());
video.setRating(videoRequest.getRating());
video.setDuration(videoRequest.getDuration());
video.setSrcUuid(videoRequest.getSrc());
video.setPosterUuid(videoRequest.getPoster());
video.setPublished(videoRequest.isPublished());
video.setCategories(videoRequest.getCategories() != null ? videoRequest.getCategories() :
List.of());
videoRepository.save(video);
return new MessageResponse("Video updated successfully.");
}
/**
* 管理员删除视频
*/
@Override
public MessageResponse deleteVideoByAdmin(Long id) {
if (!videoRepository.existsById(id)) {
throw new IllegalArgumentException("Video not found: " + id);
}
videoRepository.deleteById(id);
return new MessageResponse("Video deleted successfully.");
}
/**
* 管理员切换视频发布状态
*/
@Override
public MessageResponse toggleVideoPublishStatusByAdmin(Long id, boolean status) {
Video video = serviceUtils.getVideoByIdOrThrow(id);
video.setPublished(status);
videoRepository.save(video);
return new MessageResponse("Video publish status updated successfully.");
}
/**
* 管理员获取视频统计数据
*/
@Override
public VideoStatsResponse getAdminStats() {
long totalVideo = userRepository.count();
long publishedVideos = videoRepository.countPublishedVideos();
long totalDuration = videoRepository.getTotalDuration();
return new VideoStatsResponse(totalVideo, publishedVideos, totalDuration);
}
/**
* 普通用户获取已发布视频列表(分页、搜索、并标记当前用户的 watchlist 状态)
*/
@Override
public PageResponse<VideoResponse> getPublishedVideos(int page, int size, String search, String email) {
Pageable pageable = PaginationUtils.createPageRequest(page, size, "id");
Page<Video> videoPage;
if (search != null && !search.trim().isEmpty()) {
videoPage = videoRepository.searchPublishedVideos(search.trim(), pageable);
} else {
videoPage = videoRepository.findPublishedVideos(pageable);
}
List<Video> videos = videoPage.getContent();
Set<Long> watchlistIds = Set.of();
if (!videos.isEmpty()) {
try {
List<Long> videoIds = videos.stream().map(Video::getId).toList();
watchlistIds = userRepository.findWatchlistVideoIds(email, videoIds);
} catch (Exception e) {
watchlistIds = Set.of();
}
}
Set<Long> finalWatchlistIds = watchlistIds;
videos.forEach(video -> video.setIsInWatchlist(finalWatchlistIds.contains(video.getId())));
List<VideoResponse> videoResponses = videos.stream().map(VideoResponse::fromEntity).toList();
return PaginationUtils.toPageResponse(videoPage, videoResponses);
}
/**
* 获取推荐/精选视频(随机返回 5 个已发布视频)
*/
@Override
public List<VideoResponse> getFeaturedVideos() {
Pageable pageable = PageRequest.of(0, 5);
List<Video> videos = videoRepository.findRandomPulishedVideos(pageable);
return videos.stream().map(VideoResponse::fromEntity).toList();
}
}
⚠️ 潜在问题提醒(仅指出,不修改):
getAdminStats方法中totalVideo = userRepository.count()错误地统计了用户数量,而非视频总数。updateVideoAdmin方法使用new Video()并设置 ID 后直接save,可能导致未传入的字段被覆盖为默认值(如null)。应查询现有实体后更新。findRandomPulishedVideos方法名拼写错误(Pulished应为Published),且VideoRepository中需定义该方法。VideoStatsResponse类名与之前定义的VideoStatusResponse不一致。
三、控制器层(Controller)
3.1 VideoController - 视频管理控制器
路径:com.netflix.clone.demo.controller.VideoController
功能:提供管理员视频管理 API 和普通用户浏览视频的 API,所有管理员接口均需 ADMIN 角色权限。
package com.netflix.clone.demo.controller;
import com.netflix.clone.demo.dto.request.VideoRequest;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.dto.response.PageResponse;
import com.netflix.clone.demo.dto.response.VideoResponse;
import com.netflix.clone.demo.dto.response.VideoStatsResponse;
import com.netflix.clone.demo.service.VideoSerivice;
import jakarta.validation.Valid;
import org.hibernate.query.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/videos")
public class VideoController {
@Autowired
private VideoSerivice videoSerivice;
/**
* 管理员创建视频
*/
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/admin")
public ResponseEntity<MessageResponse> createVideoByAdmin(
@Valid @RequestBody VideoRequest videoRequest
) {
return ResponseEntity.ok(videoSerivice.createVideoByAdmin(videoRequest));
}
/**
* 管理员分页获取所有视频(支持搜索)
*/
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public ResponseEntity<PageResponse<VideoResponse>> getAllAdminVideos(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String search
) {
return ResponseEntity.ok(videoSerivice.getAllAdminVideos(page, size, search));
}
/**
* 管理员更新视频
*/
@PreAuthorize("hasRole('ADMIN')")
@PutMapping("/admin/{id}")
public ResponseEntity<MessageResponse> updateVideoByAdmin(
@PathVariable Long id, @Valid @RequestBody
VideoRequest videoRequest
) {
return ResponseEntity.ok(videoSerivice.updateVideoAdmin(id, videoRequest));
}
/**
* 管理员删除视频
*/
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/admin/{id}")
public ResponseEntity<MessageResponse> deleteVideoByAdmin(
@PathVariable Long id
) {
return ResponseEntity.ok(videoSerivice.deleteVideoByAdmin(id));
}
/**
* 管理员切换视频发布状态
*/
@PreAuthorize("hasRole('ADMIN')")
@PatchMapping("/admin/{id}/publish")
public ResponseEntity<MessageResponse> toggleVideoPublishStatusByAdmin(
@PathVariable Long id, @RequestParam boolean value
) {
return ResponseEntity.ok(videoSerivice.toggleVideoPublishStatusByAdmin(id, value));
}
/**
* 管理员获取视频统计数据
*/
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/stats")
public ResponseEntity<VideoStatsResponse> getAdminStats() {
return ResponseEntity.ok(videoSerivice.getAdminStats());
}
/**
* 普通用户获取已发布视频列表(分页、搜索、自动标记 watchlist 状态)
*/
@GetMapping("/published")
public ResponseEntity<PageResponse<VideoResponse>> getPublishedVideos(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String search,
Authentication authentication
) {
String email = authentication.getName();
PageResponse<VideoResponse> response = videoSerivice.getPublishedVideos(page, size, search, email);
return ResponseEntity.ok(response);
}
/**
* 获取精选/推荐视频(无需认证)
*/
@GetMapping("/featured")
public ResponseEntity<List<VideoResponse>> getFeaturedVideos() {
List<VideoResponse> response = videoSerivice.getFeaturedVideos();
return ResponseEntity.ok(response);
}
}
⚠️ 潜在问题提醒(仅指出,不修改):
- 导入了未使用的
org.hibernate.query.Page,应删除。VideoStatsResponse类名与之前定义的VideoStatusResponse不一致,可能导致编译错误。
四、依赖的 Repository 方法说明
为使 VideoServiceImpl 正常编译运行,VideoRepository 和 UserRepository 需补充以下方法声明(此处不提供代码,仅说明):
| Repository | 方法 | 用途 |
|---|---|---|
VideoRepository | Page<Video> searchVideos(String search, Pageable pageable) | 管理员搜索视频(标题、描述等) |
VideoRepository | Page<Video> searchPublishedVideos(String search, Pageable pageable) | 用户搜索已发布视频 |
VideoRepository | Page<Video> findPublishedVideos(Pageable pageable) | 分页获取已发布视频 |
VideoRepository | long countPublishedVideos() | 统计已发布视频数量 |
VideoRepository | long getTotalDuration() | 获取所有视频总时长 |
VideoRepository | List<Video> findRandomPulishedVideos(Pageable pageable) | 随机获取已发布视频 |
UserRepository | Set<Long> findWatchlistVideoIds(String email, List<Long> videoIds) | 查询用户 watchlist 中包含的视频 ID 集合 |
五、Postman API 测试指南
5.1 基础配置
- Base URL:
http://localhost:8080 - 认证要求:
- 所有
/api/videos/admin/**接口需要ADMIN角色。 /api/videos/published需要用户认证(JWT Token)。/api/videos/featured无需认证。
- 所有
5.2 获取 JWT Token
管理员登录
请求:
- Method:
POST - URL:
http://localhost:8080/api/auth/login - Body (JSON):
{ "email": "admin@example.com", "password": "adminPassword" }
响应:
{
"token": "eyJhbGciOiJIUzM4NCJ9...",
"email": "admin@example.com",
"fullName": "Admin User",
"role": "ADMIN"
}
普通用户登录
同理,获取普通用户 Token 用于测试 /published 接口。
5.3 管理员接口测试
以下接口均需在 Headers 中添加 Authorization: Bearer <admin_token>。
① 创建视频 – POST /api/videos/admin
请求体:
{
"title": "The Matrix",
"description": "A computer hacker learns about the true nature of reality...",
"year": 1999,
"rating": "R",
"duration": 8160,
"src": "matrix-video-uuid",
"poster": "matrix-poster-uuid",
"published": true,
"categories": ["Action", "Sci-Fi"]
}
预期响应:
{
"message": "Video created successfully."
}
② 分页获取所有视频 – GET /api/videos/admin
请求示例:
- 无搜索:
GET /api/videos/admin?page=0&size=10 - 带搜索:
GET /api/videos/admin?page=0&size=10&search=Matrix
预期响应:
{
"content": [
{
"id": 1,
"title": "The Matrix",
"description": "...",
"year": 1999,
"rating": "R",
"duration": 8160,
"src": "http://localhost:8080/api/files/video/matrix-video-uuid",
"poster": "http://localhost:8080/api/files/image/matrix-poster-uuid",
"published": true,
"categories": ["Action", "Sci-Fi"],
"createdAt": "2026-04-19T10:00:00Z",
"updatedAt": "2026-04-19T10:00:00Z",
"isInWatchlist": null
}
],
"totalElements": 100,
"totalPages": 10,
"number": 0,
"size": 10
}
③ 更新视频 – PUT /api/videos/admin/{id}
请求体:同创建视频。
④ 删除视频 – DELETE /api/videos/admin/{id}
预期响应:
{
"message": "Video deleted successfully."
}
⑤ 切换发布状态 – PATCH /api/videos/admin/{id}/publish?value=true
预期响应:
{
"message": "Video publish status updated successfully."
}
⑥ 获取统计数据 – GET /api/videos/admin/stats
预期响应:
{
"totalVideos": 1520,
"publishedVideos": 1325,
"totalDuration": 489600
}
5.4 普通用户接口测试
⑦ 获取已发布视频列表 – GET /api/videos/published
Headers:Authorization: Bearer <user_token>
请求示例:
GET /api/videos/published?page=0&size=10GET /api/videos/published?search=action&page=0&size=10
预期响应:格式同管理员分页查询,但 isInWatchlist 字段会根据当前用户的实际 watchlist 状态返回 true 或 false。
⑧ 获取精选视频 – GET /api/videos/featured
无需认证。
预期响应:
[
{
"id": 5,
"title": "Featured Movie 1",
...
"isInWatchlist": false
},
... (最多 5 条)
]
5.5 Postman 环境变量建议
| 变量名 | 示例值 |
|---|---|
baseUrl | http://localhost:8080 |
adminToken | 管理员登录后获取的 JWT |
userToken | 普通用户登录后获取的 JWT |
videoId | 已存在的视频 ID |
评论