feat(media): 支持后台转码配置与缩略图轴

This commit is contained in:
Zhou Weidong committed 2026-10-22 11:20:28 +08:00
1 parent 70da939a58
commit 316baf28d6
39 files changed
+1523 -38

No files matched your search

@@ -5,7 +5,10 @@ package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.security.ImpersonationGuard;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.media.config.MediaTranscodeProfile;
import com.fruitshorts.media.service.MediaService;
import com.fruitshorts.media.service.MediaTranscodeProfileService;
import com.fruitshorts.media.vo.MediaAssetRespVO;
import com.fruitshorts.media.vo.UploadCompleteReqVO;
import com.fruitshorts.media.vo.UploadInitReqVO;
@@ -19,6 +22,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -35,6 +39,7 @@ import org.springframework.web.multipart.MultipartFile;
public class MediaController {
private final MediaService mediaService;
private final MediaTranscodeProfileService profileService;
@Operation(summary = "初始化分片上传")
@PostMapping("/upload/init")
@@ -78,4 +83,21 @@ public class MediaController {
public Result<MediaAssetRespVO> get(@PathVariable Long id) {
return Result.ok(mediaService.get(id));
}
@Operation(summary = "读取转码档位配置")
@GetMapping("/transcode-profile")
@PreAuthorize("@ss.hasPermission('episode:upload')")
public Result<MediaTranscodeProfile> transcodeProfile() {
return Result.ok(profileService.readConfiguredProfile());
}
@Operation(summary = "保存转码档位配置")
@PutMapping("/transcode-profile")
@PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "后台管理", action = "修改转码设置")
public Result<MediaTranscodeProfile> saveTranscodeProfile(
@Valid @RequestBody MediaTranscodeProfile profile) {
ImpersonationGuard.rejectWrite();
return Result.ok(profileService.save(profile));
}
}
@@ -0,0 +1,98 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fruitshorts.admin.support.ControllerContractSupport;
import com.fruitshorts.media.config.MediaTranscodeProfile;
import com.fruitshorts.media.config.TranscodeProperties;
import com.fruitshorts.media.service.MediaService;
import com.fruitshorts.media.service.MediaTranscodeProfileService;
import com.fruitshorts.system.service.ConfigService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 后台转码配置接口:默认值、非法拒绝与成功落库。
*/
class MediaTranscodeProfileContractTest {
private ConfigService configService;
private ObjectMapper objectMapper;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
configService = mock(ConfigService.class);
objectMapper = new ObjectMapper();
MediaTranscodeProfileService profileService = new MediaTranscodeProfileService(
configService, objectMapper, new TranscodeProperties());
MediaController controller = new MediaController(mock(MediaService.class), profileService);
mockMvc = ControllerContractSupport.mockMvcOf(controller);
given(configService.get(eq(MediaTranscodeProfileService.CONFIG_KEY), anyString()))
.willReturn("");
}
@Test
void should_return_default_profile_when_config_is_missing() throws Exception {
mockMvc.perform(get("/admin/media/transcode-profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.segmentSeconds").value(6))
.andExpect(jsonPath("$.data.ladder.length()").value(4))
.andExpect(jsonPath("$.data.ladder[0].quality").value("360p"))
.andExpect(jsonPath("$.data.sprite.columns").value(10));
}
@Test
void should_reject_illegal_profile_without_writing_config() throws Exception {
MediaTranscodeProfile profile = new MediaTranscodeProfile();
profile.getLadder().get(0).setWidth(641);
mockMvc.perform(put("/admin/media/transcode-profile")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(profile)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.message").value("宽高必须为正偶数"));
verify(configService, never()).update(any());
}
@Test
void should_persist_profile_when_request_is_valid() throws Exception {
MediaTranscodeProfile profile = new MediaTranscodeProfile();
profile.setSegmentSeconds(4);
mockMvc.perform(put("/admin/media/transcode-profile")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(profile)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.segmentSeconds").value(4));
@SuppressWarnings("unchecked")
ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
verify(configService).update(captor.capture());
assertThat(captor.getValue().get(MediaTranscodeProfileService.CONFIG_KEY))
.contains("\"segmentSeconds\":4");
}
}