diff --git a/src/main/java/com/ycwl/basic/integration/CLAUDE.md b/src/main/java/com/ycwl/basic/integration/CLAUDE.md index 50d24bab..aa4b9da9 100644 --- a/src/main/java/com/ycwl/basic/integration/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/integration/CLAUDE.md @@ -1161,6 +1161,228 @@ fallbackService.clearAllFallbackCache("zt-render-worker"); - **Active (isActive=1)**: Worker is available for tasks - **Inactive (isActive=0)**: Worker is disabled +## Render Template Integration (ZT-Render-Worker Microservice) + +### Key Components + +#### Feign Clients +- **RenderTemplateV2Client**: Template CRUD operations, segment management + +#### Services +- **RenderTemplateIntegrationService**: High-level template operations (with automatic fallback for queries) + +### Usage Examples + +#### Basic Template Operations +```java +@Autowired +private RenderTemplateIntegrationService templateService; + +// Create template (direct operation, fails immediately on error) +CreateTemplateRequest createRequest = new CreateTemplateRequest(); +createRequest.setScenicId(1001L); +createRequest.setName("新年贺卡模板"); +createRequest.setDescription("用于新年祝福的模板"); +createRequest.setDefaultDurationMs(10000L); + +OutputSpecDTO outputSpec = new OutputSpecDTO(); +outputSpec.setWidth(1080); +outputSpec.setHeight(1920); +outputSpec.setFps(30); +createRequest.setOutputSpec(outputSpec); + +TemplateV2DTO template = templateService.createTemplate(createRequest); + +// Get template details (automatically falls back to cache on failure) +TemplateV2DTO templateInfo = templateService.getTemplate(templateId); + +// Get template with segments (automatically falls back to cache on failure) +TemplateV2WithSegmentsDTO templateWithSegments = templateService.getTemplateWithSegments(templateId); + +// List templates (no fallback for list operations) +PageResponse templates = templateService.listTemplates(1, 10, scenicId, 1, null); + +// Update template (direct operation, fails immediately on error) +UpdateTemplateRequest updateRequest = new UpdateTemplateRequest(); +updateRequest.setName("更新后的模板名称"); +templateService.updateTemplate(templateId, updateRequest); + +// Publish template (direct operation, fails immediately on error) +templateService.publishTemplate(templateId); + +// Create new version (direct operation, fails immediately on error) +TemplateV2DTO newVersion = templateService.createTemplateVersion(templateId); + +// Delete template (direct operation, fails immediately on error) +templateService.deleteTemplate(templateId); +``` + +#### Segment Management +```java +// Get template segments (automatically falls back to cache on failure) +List segments = templateService.getTemplateSegments(templateId); + +// Create segment (direct operation, fails immediately on error) +CreateSegmentRequest segmentRequest = new CreateSegmentRequest(); +segmentRequest.setSegmentIndex(0); +segmentRequest.setSegmentType("RENDER"); +segmentRequest.setSourceType("SLOT"); +segmentRequest.setSourceRef("slot1"); +segmentRequest.setDurationMs(2000L); +segmentRequest.setTransitionType("fade"); +segmentRequest.setTransitionMs(500); + +RenderSpecDTO renderSpec = new RenderSpecDTO(); +renderSpec.setCropEnable(true); +renderSpec.setSpeed("1.0"); +segmentRequest.setRenderSpec(renderSpec); + +TemplateV2SegmentDTO segment = templateService.createSegment(templateId, segmentRequest); + +// Update segment (direct operation, fails immediately on error) +UpdateSegmentRequest updateSegmentRequest = new UpdateSegmentRequest(); +updateSegmentRequest.setDurationMs(3000L); +templateService.updateSegment(templateId, segmentId, updateSegmentRequest); + +// Delete segment (direct operation, fails immediately on error) +templateService.deleteSegment(templateId, segmentId); + +// Replace all segments (direct operation, fails immediately on error) +ReplaceSegmentsRequest replaceRequest = new ReplaceSegmentsRequest(); +replaceRequest.setSegments(Arrays.asList(segmentRequest1, segmentRequest2)); +templateService.replaceSegments(templateId, replaceRequest); +``` + +### Template Status +- **0**: Draft - Template is being edited +- **1**: Published - Template is live and available for rendering + +### Segment Types +- **FIXED**: Fixed asset segment +- **RENDER**: Segment that needs to be rendered with user materials + +### Source Types +- **ASSET**: Fixed asset resource +- **PLACEHOLDER_VIDEO**: Video placeholder slot +- **PLACEHOLDER_IMAGE**: Image placeholder slot +- **SLOT**: Material slot + +## Render Job Integration (ZT-Render-Worker Microservice) + +### Key Components + +#### Feign Clients +- **RenderJobV2Client**: Job creation, status queries, admin operations + +#### Services +- **RenderJobIntegrationService**: High-level job operations (with automatic fallback for queries) + +### Usage Examples + +#### Creating and Managing Render Jobs +```java +@Autowired +private RenderJobIntegrationService jobService; + +// Create preview job (direct operation, fails immediately on error) +CreatePreviewRequest previewRequest = new CreatePreviewRequest(); +previewRequest.setTemplateId(123L); +previewRequest.setScenicId(456L); +previewRequest.setFaceId(789L); +previewRequest.setMemberId(101L); + +// Set materials by slot +Map> materialsBySlot = new HashMap<>(); +MaterialDTO material = new MaterialDTO(); +material.setUrl("https://example.com/video.mp4"); +material.setType("video"); +material.setDuration(5000L); +materialsBySlot.put("slot1", Arrays.asList(material)); +previewRequest.setMaterialsBySlot(materialsBySlot); + +CreatePreviewResponse previewResponse = jobService.createPreview(previewRequest); +log.info("作业创建成功, jobId: {}, playUrl: {}", previewResponse.getJobId(), previewResponse.getPlayUrl()); + +// Get job status (automatically falls back to cache on failure) +JobStatusResponse status = jobService.getJobStatus(jobId); +log.info("作业状态: {}, 进度: {}%", status.getStatus(), status.getProgress()); + +// Get playlist info (automatically falls back to cache on failure) +PlaylistInfoDTO playlistInfo = jobService.getPlaylistInfo(jobId); +log.info("总片段: {}, 已发布: {}", playlistInfo.getSegmentCount(), playlistInfo.getPublishedCount()); + +// Get HLS playlist (direct call, returns M3U8 content) +String hlsPlaylist = jobService.getHlsPlaylist(jobId); + +// Cancel job (direct operation, fails immediately on error) +jobService.cancelJob(jobId); +``` + +#### Admin Operations +```java +// List jobs (no fallback for list operations) +PageResponse jobs = jobService.listJobs(scenicId, templateId, "RUNNING", "PREVIEW", 1, 20); +log.info("查询到 {} 条作业", jobs.getTotal()); + +// Get job detail (automatically falls back to cache on failure) +RenderJobV2DTO jobDetail = jobService.getJobDetail(jobId); +log.info("作业详情: templateId={}, status={}", jobDetail.getTemplateId(), jobDetail.getStatus()); + +// Get job segments (automatically falls back to cache on failure) +List jobSegments = jobService.getJobSegments(jobId); +for (RenderJobSegmentV2DTO segment : jobSegments) { + log.info("片段 {}: status={}, tsUrl={}", segment.getPlanSegmentIndex(), segment.getStatus(), segment.getTsUrl()); +} +``` + +### Job Status +- **PENDING**: Job is waiting to be processed +- **RUNNING**: Job is currently being processed +- **SUCCESS**: Job completed successfully +- **FAILED**: Job failed +- **CANCELED**: Job was canceled + +### Job Types +- **PREVIEW**: Preview job (HLS streaming) +- **PREVIEW_HLS**: HLS preview job +- **FINAL_MP4**: Final MP4 export job + +### Segment Status +- **PENDING**: Segment is waiting to be processed +- **RENDERING**: Segment is being rendered +- **VIDEO_READY**: Video is ready +- **PACKAGING**: Segment is being packaged +- **TS_READY**: TS file is ready +- **PUBLISHED**: Segment is published and available +- **FAILED**: Segment processing failed + +### Fallback Cache Management for Templates and Jobs +```java +@Autowired +private IntegrationFallbackService fallbackService; + +// Check fallback cache status for templates +boolean hasTemplateCache = fallbackService.hasFallbackCache("zt-render-worker", "template:1001"); +boolean hasTemplateSegmentsCache = fallbackService.hasFallbackCache("zt-render-worker", "template:segments:1001"); + +// Check fallback cache status for jobs +boolean hasJobStatusCache = fallbackService.hasFallbackCache("zt-render-worker", "job:status:2001"); +boolean hasJobDetailCache = fallbackService.hasFallbackCache("zt-render-worker", "job:detail:2001"); + +// Get cache statistics +IntegrationFallbackService.FallbackCacheStats stats = + fallbackService.getFallbackCacheStats("zt-render-worker"); +log.info("Render fallback cache: {} items, TTL: {} days", + stats.getTotalCacheCount(), stats.getFallbackTtlDays()); + +// Clear specific cache +fallbackService.clearFallbackCache("zt-render-worker", "template:1001"); +fallbackService.clearFallbackCache("zt-render-worker", "job:status:2001"); + +// Clear all render worker caches +fallbackService.clearAllFallbackCache("zt-render-worker"); +``` + ## ZT-Message Integration (Kafka Producer) ### Overview diff --git a/src/main/java/com/ycwl/basic/integration/render/client/RenderJobV2Client.java b/src/main/java/com/ycwl/basic/integration/render/client/RenderJobV2Client.java new file mode 100644 index 00000000..c8b1e19d --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/client/RenderJobV2Client.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.integration.render.client; + +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.common.response.PageResponse; +import com.ycwl.basic.integration.render.dto.job.*; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 渲染作业V2客户端 + */ +@FeignClient(name = "zt-render-worker", contextId = "render-job-v2", path = "/api/render/v2") +public interface RenderJobV2Client { + + // ==================== 小程序侧接口 ==================== + + /** + * 创建预览作业 + */ + @PostMapping("/preview") + CommonResponse createPreview(@RequestBody CreatePreviewRequest request); + + /** + * 获取作业状态 + */ + @GetMapping("/jobs/{jobId}") + CommonResponse getJobStatus(@PathVariable("jobId") Long jobId); + + /** + * 获取HLS播放列表 + * 返回M3U8格式的文本内容 + */ + @GetMapping("/jobs/{jobId}/index.m3u8") + String getHlsPlaylist(@PathVariable("jobId") Long jobId); + + /** + * 获取播放列表信息 + */ + @GetMapping("/jobs/{jobId}/playlist-info") + CommonResponse getPlaylistInfo(@PathVariable("jobId") Long jobId); + + /** + * 取消作业 + */ + @PostMapping("/jobs/{jobId}/cancel") + CommonResponse cancelJob(@PathVariable("jobId") Long jobId); + + // ==================== 管理端接口 ==================== + + /** + * 获取作业列表 + */ + @GetMapping("/admin/jobs") + CommonResponse> listJobs( + @RequestParam(required = false) Long scenicId, + @RequestParam(required = false) Long templateId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String jobType, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize); + + /** + * 获取作业详情 + */ + @GetMapping("/admin/jobs/{jobId}") + CommonResponse getJobDetail(@PathVariable("jobId") Long jobId); + + /** + * 获取作业片段列表 + */ + @GetMapping("/admin/jobs/{jobId}/segments") + CommonResponse> getJobSegments(@PathVariable("jobId") Long jobId); +} diff --git a/src/main/java/com/ycwl/basic/integration/render/client/RenderTemplateV2Client.java b/src/main/java/com/ycwl/basic/integration/render/client/RenderTemplateV2Client.java new file mode 100644 index 00000000..1c1683bf --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/client/RenderTemplateV2Client.java @@ -0,0 +1,111 @@ +package com.ycwl.basic.integration.render.client; + +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.common.response.PageResponse; +import com.ycwl.basic.integration.render.dto.template.*; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 渲染模板V2客户端 + */ +@FeignClient(name = "zt-render-worker", contextId = "render-template-v2", path = "/api/render/template/v2") +public interface RenderTemplateV2Client { + + // ==================== Template CRUD Operations ==================== + + /** + * 创建模板 + */ + @PostMapping + CommonResponse createTemplate(@RequestBody CreateTemplateRequest request); + + /** + * 获取模板列表 + */ + @GetMapping + CommonResponse> listTemplates( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Long scenicId, + @RequestParam(required = false) Integer status, + @RequestParam(required = false) String name); + + /** + * 获取模板详情 + */ + @GetMapping("/{id}") + CommonResponse getTemplate(@PathVariable("id") Long id); + + /** + * 获取模板及其片段 + */ + @GetMapping("/{id}/with-segments") + CommonResponse getTemplateWithSegments(@PathVariable("id") Long id); + + /** + * 更新模板 + */ + @PutMapping("/{id}") + CommonResponse updateTemplate(@PathVariable("id") Long id, + @RequestBody UpdateTemplateRequest request); + + /** + * 删除模板 + */ + @DeleteMapping("/{id}") + CommonResponse deleteTemplate(@PathVariable("id") Long id); + + // ==================== Template Operations ==================== + + /** + * 发布模板 + */ + @PostMapping("/{id}/publish") + CommonResponse publishTemplate(@PathVariable("id") Long id); + + /** + * 创建新版本 + */ + @PostMapping("/{id}/version") + CommonResponse createTemplateVersion(@PathVariable("id") Long id); + + // ==================== Segment Management ==================== + + /** + * 获取模板片段列表 + */ + @GetMapping("/{id}/segments") + CommonResponse> getTemplateSegments(@PathVariable("id") Long id); + + /** + * 创建片段 + */ + @PostMapping("/{id}/segments") + CommonResponse createSegment(@PathVariable("id") Long id, + @RequestBody CreateSegmentRequest request); + + /** + * 更新片段 + */ + @PutMapping("/{id}/segments/{segmentId}") + CommonResponse updateSegment(@PathVariable("id") Long id, + @PathVariable("segmentId") Long segmentId, + @RequestBody UpdateSegmentRequest request); + + /** + * 删除片段 + */ + @DeleteMapping("/{id}/segments/{segmentId}") + CommonResponse deleteSegment(@PathVariable("id") Long id, + @PathVariable("segmentId") Long segmentId); + + /** + * 替换所有片段 + */ + @PostMapping("/{id}/segments/replace") + CommonResponse replaceSegments(@PathVariable("id") Long id, + @RequestBody ReplaceSegmentsRequest request); +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/common/AudioSpecDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/common/AudioSpecDTO.java new file mode 100644 index 00000000..110224a5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/common/AudioSpecDTO.java @@ -0,0 +1,45 @@ +package com.ycwl.basic.integration.render.dto.common; + +import lombok.Data; + +/** + * 音频参数DTO + */ +@Data +public class AudioSpecDTO { + + /** + * 音频素材URL + */ + private String audioUrl; + + /** + * 音量 (0.0-1.0) + */ + private Double volume; + + /** + * 淡入时长(毫秒) + */ + private Integer fadeInMs; + + /** + * 淡出时长(毫秒) + */ + private Integer fadeOutMs; + + /** + * 音频开始位置(毫秒) + */ + private Integer startMs; + + /** + * 延迟播放(毫秒) + */ + private Integer delayMs; + + /** + * 是否循环 + */ + private Boolean loopEnable; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/common/OutputSpecDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/common/OutputSpecDTO.java new file mode 100644 index 00000000..706f4128 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/common/OutputSpecDTO.java @@ -0,0 +1,50 @@ +package com.ycwl.basic.integration.render.dto.common; + +import lombok.Data; + +/** + * 输出规格DTO + */ +@Data +public class OutputSpecDTO { + + /** + * 宽度 + */ + private Integer width; + + /** + * 高度 + */ + private Integer height; + + /** + * 帧率 + */ + private Integer fps; + + /** + * 比特率 + */ + private Integer bitrate; + + /** + * 视频编解码器 (默认h264) + */ + private String codec; + + /** + * 音频编解码器 (默认aac) + */ + private String audioCodec; + + /** + * 采样率 (默认48000) + */ + private Integer sampleRate; + + /** + * 声道数 (默认2) + */ + private Integer channels; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/common/RenderSpecDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/common/RenderSpecDTO.java new file mode 100644 index 00000000..0a447225 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/common/RenderSpecDTO.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.integration.render.dto.common; + +import lombok.Data; + +/** + * 渲染参数DTO + */ +@Data +public class RenderSpecDTO { + + /** + * 是否启用人脸裁切 + */ + private Boolean cropEnable; + + /** + * 裁切后大小 + */ + private String cropSize; + + /** + * 倍速 + */ + private String speed; + + /** + * 调色LUT文件URL + */ + private String lutUrl; + + /** + * 叠加蒙版URL + */ + private String overlayUrl; + + /** + * 特效配置 + */ + private String effects; + + /** + * 是否缩放裁切 + */ + private Boolean zoomCut; + + /** + * 竖屏切割位置 + */ + private String videoCrop; + + /** + * 人脸位置参数 + */ + private String facePos; + + /** + * 转场效果 + */ + private String transitions; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/CreatePreviewRequest.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/CreatePreviewRequest.java new file mode 100644 index 00000000..abeef19b --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/CreatePreviewRequest.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.integration.render.dto.job; + +import com.ycwl.basic.integration.render.dto.common.OutputSpecDTO; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 创建预览请求 + */ +@Data +public class CreatePreviewRequest { + + /** + * 模板ID (必填) + */ + private Long templateId; + + /** + * 景区ID (必填) + */ + private Long scenicId; + + /** + * 人脸ID (可选) + */ + private Long faceId; + + /** + * 会员ID (可选) + */ + private Long memberId; + + /** + * 素材槽映射 (可选) + * key: 槽位键 + * value: 素材列表 + */ + private Map> materialsBySlot; + + /** + * 自定义输出规格 (可选) + */ + private OutputSpecDTO outputSpec; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/CreatePreviewResponse.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/CreatePreviewResponse.java new file mode 100644 index 00000000..47709b3d --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/CreatePreviewResponse.java @@ -0,0 +1,25 @@ +package com.ycwl.basic.integration.render.dto.job; + +import lombok.Data; + +/** + * 创建预览响应 + */ +@Data +public class CreatePreviewResponse { + + /** + * 作业ID + */ + private Long jobId; + + /** + * 状态 + */ + private String status; + + /** + * 播放URL + */ + private String playUrl; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/JobStatusResponse.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/JobStatusResponse.java new file mode 100644 index 00000000..d0ec3016 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/JobStatusResponse.java @@ -0,0 +1,55 @@ +package com.ycwl.basic.integration.render.dto.job; + +import lombok.Data; + +/** + * 作业状态响应 + */ +@Data +public class JobStatusResponse { + + /** + * 作业ID + */ + private Long jobId; + + /** + * 状态 (PENDING, RUNNING, SUCCESS, FAILED, CANCELED) + */ + private String status; + + /** + * 进度 (0.0 - 100.0) + */ + private Double progress; + + /** + * 总片段数 + */ + private Integer segmentCount; + + /** + * 已发布片段数 + */ + private Integer publishedCount; + + /** + * 播放URL + */ + private String playUrl; + + /** + * MP4下载URL + */ + private String mp4Url; + + /** + * 错误码 + */ + private String errorCode; + + /** + * 错误信息 + */ + private String errorMessage; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/MaterialDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/MaterialDTO.java new file mode 100644 index 00000000..3816bf88 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/MaterialDTO.java @@ -0,0 +1,35 @@ +package com.ycwl.basic.integration.render.dto.job; + +import lombok.Data; + +/** + * 素材信息DTO + */ +@Data +public class MaterialDTO { + + /** + * 素材URL + */ + private String url; + + /** + * 类型 (video, image) + */ + private String type; + + /** + * 时长(毫秒) + */ + private Long duration; + + /** + * 人脸分数 + */ + private String score; + + /** + * 人脸位置 + */ + private String facePos; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/PlaylistInfoDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/PlaylistInfoDTO.java new file mode 100644 index 00000000..ce552dda --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/PlaylistInfoDTO.java @@ -0,0 +1,25 @@ +package com.ycwl.basic.integration.render.dto.job; + +import lombok.Data; + +/** + * 播放列表信息DTO + */ +@Data +public class PlaylistInfoDTO { + + /** + * 总片段数 + */ + private Integer segmentCount; + + /** + * 进度 (0.0 - 100.0) + */ + private Double progress; + + /** + * 已发布片段数 + */ + private Integer publishedCount; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/RenderJobSegmentV2DTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/RenderJobSegmentV2DTO.java new file mode 100644 index 00000000..67ef3009 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/RenderJobSegmentV2DTO.java @@ -0,0 +1,112 @@ +package com.ycwl.basic.integration.render.dto.job; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ycwl.basic.integration.render.dto.common.AudioSpecDTO; +import com.ycwl.basic.integration.render.dto.common.RenderSpecDTO; +import lombok.Data; + +import java.util.Date; + +/** + * 渲染作业片段V2 DTO + */ +@Data +public class RenderJobSegmentV2DTO { + + /** + * 片段ID + */ + private Long id; + + /** + * 作业ID + */ + private Long jobId; + + /** + * 模板片段索引 + */ + private Integer templateSegmentIndex; + + /** + * 计划片段索引 + */ + private Integer planSegmentIndex; + + /** + * 开始时间(毫秒) + */ + private Long startTimeMs; + + /** + * 时长(毫秒) + */ + private Long durationMs; + + /** + * 片段类型 (RENDER, FIXED) + */ + private String segmentType; + + /** + * 状态 (PENDING, RENDERING, VIDEO_READY, PACKAGING, TS_READY, PUBLISHED, FAILED) + */ + private String status; + + /** + * 素材来源类型 + */ + private String sourceType; + + /** + * 素材来源引用 + */ + private String sourceRef; + + /** + * 绑定素材URL + */ + private String boundMaterialUrl; + + /** + * 渲染参数JSON + */ + @JsonProperty("renderSpecJson") + private RenderSpecDTO renderSpecJson; + + /** + * 音频参数JSON + */ + @JsonProperty("audioSpecJson") + private AudioSpecDTO audioSpecJson; + + /** + * 视频URL + */ + private String videoUrl; + + /** + * TS文件URL + */ + private String tsUrl; + + /** + * TS时长(秒) + */ + private Double tsDuration; + + /** + * 创建时间 + */ + @JsonProperty("createTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonProperty("updateTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/job/RenderJobV2DTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/job/RenderJobV2DTO.java new file mode 100644 index 00000000..0cf15796 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/job/RenderJobV2DTO.java @@ -0,0 +1,144 @@ +package com.ycwl.basic.integration.render.dto.job; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 渲染作业V2 DTO + */ +@Data +public class RenderJobV2DTO { + + /** + * 作业ID + */ + private Long id; + + /** + * 作业类型 (PREVIEW, PREVIEW_HLS, FINAL_MP4) + */ + private String jobType; + + /** + * 状态 (PENDING, RUNNING, SUCCESS, FAILED, CANCELED) + */ + private String status; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 模板ID + */ + private Long templateId; + + /** + * 模板版本 + */ + private Integer templateVersion; + + /** + * 人脸ID + */ + private Long faceId; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 总时长(毫秒) + */ + private Long totalDurationMs; + + /** + * 输出规格JSON + */ + @JsonProperty("outputSpecJson") + private Map outputSpecJson; + + /** + * 渲染计划JSON + */ + private String planJson; + + /** + * 总片段数 + */ + private Integer segmentCount; + + /** + * 已发布片段数 + */ + private Integer publishedCount; + + /** + * 已完成片段数 + */ + private Integer completedCount; + + /** + * M3U8播放地址 + */ + private String m3u8Url; + + /** + * 音频URL + */ + private String audioUrl; + + /** + * MP4下载地址 + */ + private String mp4Url; + + /** + * 错误码 + */ + private String errorCode; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 幂等键 + */ + private String idempotencyKey; + + /** + * 开始时间 + */ + @JsonProperty("startTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date startTime; + + /** + * 完成时间 + */ + @JsonProperty("finishTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date finishTime; + + /** + * 创建时间 + */ + @JsonProperty("createTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonProperty("updateTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/CreateSegmentRequest.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/CreateSegmentRequest.java new file mode 100644 index 00000000..5b17b98a --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/CreateSegmentRequest.java @@ -0,0 +1,84 @@ +package com.ycwl.basic.integration.render.dto.template; + +import com.ycwl.basic.integration.render.dto.common.AudioSpecDTO; +import com.ycwl.basic.integration.render.dto.common.RenderSpecDTO; +import lombok.Data; + +import java.util.Map; + +/** + * 创建片段请求 + */ +@Data +public class CreateSegmentRequest { + + /** + * 片段索引 + */ + private Integer segmentIndex; + + /** + * 片段类型 (RENDER, FIXED) + */ + private String segmentType; + + /** + * 是否可缓存 + */ + private Boolean cacheable; + + /** + * 位置是否固定 + */ + private Boolean positionFixed; + + /** + * 素材来源类型 (ASSET, PLACEHOLDER_VIDEO, PLACEHOLDER_IMAGE, SLOT) + */ + private String sourceType; + + /** + * 素材来源引用 + */ + private String sourceRef; + + /** + * 时长(毫秒) + */ + private Long durationMs; + + /** + * 转场类型 + */ + private String transitionType; + + /** + * 转场时长(毫秒) + */ + private Integer transitionMs; + + /** + * 素材槽位键 + */ + private String slotKey; + + /** + * 条件表达式 + */ + private Map onlyIfExpr; + + /** + * 渲染参数 + */ + private RenderSpecDTO renderSpec; + + /** + * 音频参数 + */ + private AudioSpecDTO audioSpec; + + /** + * 扩展属性 + */ + private Map extendedProps; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/CreateTemplateRequest.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/CreateTemplateRequest.java new file mode 100644 index 00000000..9b4c06db --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/CreateTemplateRequest.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.integration.render.dto.template; + +import com.ycwl.basic.integration.render.dto.common.OutputSpecDTO; +import lombok.Data; + +/** + * 创建模板请求 + */ +@Data +public class CreateTemplateRequest { + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 模板名称 + */ + private String name; + + /** + * 描述 + */ + private String description; + + /** + * 缩略图URL + */ + private String thumbnailUrl; + + /** + * 默认时长(毫秒) + */ + private Long defaultDurationMs; + + /** + * 输出规格 + */ + private OutputSpecDTO outputSpec; + + /** + * 背景音乐URL + */ + private String bgmUrl; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/ReplaceSegmentsRequest.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/ReplaceSegmentsRequest.java new file mode 100644 index 00000000..55bffd0a --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/ReplaceSegmentsRequest.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.integration.render.dto.template; + +import lombok.Data; + +import java.util.List; + +/** + * 替换所有片段请求 + */ +@Data +public class ReplaceSegmentsRequest { + + /** + * 片段列表 + */ + private List segments; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2DTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2DTO.java new file mode 100644 index 00000000..ecd7fcdf --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2DTO.java @@ -0,0 +1,116 @@ +package com.ycwl.basic.integration.render.dto.template; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ycwl.basic.integration.render.dto.common.OutputSpecDTO; +import lombok.Data; + +import java.util.Date; + +/** + * 渲染模板V2 DTO + */ +@Data +public class TemplateV2DTO { + + /** + * 模板ID + */ + private Long id; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 模板名称 + */ + private String name; + + /** + * 版本号 + */ + private Integer version; + + /** + * 状态 (0-草稿, 1-已发布) + */ + private Integer status; + + /** + * 描述 + */ + private String description; + + /** + * 缩略图URL + */ + private String thumbnailUrl; + + /** + * 默认时长(毫秒) + */ + private Long defaultDurationMs; + + /** + * 输出规格 + */ + private OutputSpecDTO outputSpec; + + /** + * 输出宽度 + */ + private Integer outputWidth; + + /** + * 输出高度 + */ + private Integer outputHeight; + + /** + * 输出帧率 + */ + private Integer outputFps; + + /** + * 背景音乐URL + */ + private String bgmUrl; + + /** + * 背景音乐是否固定 + */ + private Boolean bgmFixed; + + /** + * 封面URL + */ + private String coverUrl; + + /** + * 创建时间 + */ + @JsonProperty("createTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonProperty("updateTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + + /** + * 删除标记 + */ + private Integer deleted; + + /** + * 删除时间 + */ + @JsonProperty("deletedAt") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date deletedAt; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2SegmentDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2SegmentDTO.java new file mode 100644 index 00000000..8966bb48 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2SegmentDTO.java @@ -0,0 +1,115 @@ +package com.ycwl.basic.integration.render.dto.template; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ycwl.basic.integration.render.dto.common.AudioSpecDTO; +import com.ycwl.basic.integration.render.dto.common.RenderSpecDTO; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 模板片段V2 DTO + */ +@Data +public class TemplateV2SegmentDTO { + + /** + * 片段ID + */ + private Long id; + + /** + * 模板ID + */ + private Long templateId; + + /** + * 片段索引 + */ + private Integer segmentIndex; + + /** + * 片段类型 (RENDER, FIXED) + */ + private String segmentType; + + /** + * 是否可缓存 + */ + private Boolean cacheable; + + /** + * 位置是否固定 + */ + private Boolean positionFixed; + + /** + * 时长(毫秒) + */ + private Long durationMs; + + /** + * 转场类型 + */ + private String transitionType; + + /** + * 转场时长(毫秒) + */ + private Integer transitionMs; + + /** + * 素材槽位键 + */ + private String slotKey; + + /** + * 条件表达式JSON + */ + @JsonProperty("onlyIfExprJson") + private Map onlyIfExprJson; + + /** + * 素材来源类型 (ASSET, PLACEHOLDER_VIDEO, PLACEHOLDER_IMAGE, SLOT) + */ + private String sourceType; + + /** + * 素材来源引用 + */ + private String sourceRef; + + /** + * 渲染参数JSON + */ + @JsonProperty("renderSpecJson") + private RenderSpecDTO renderSpecJson; + + /** + * 音频参数JSON + */ + @JsonProperty("audioSpecJson") + private AudioSpecDTO audioSpecJson; + + /** + * 扩展属性JSON + */ + @JsonProperty("extendedPropsJson") + private Map extendedPropsJson; + + /** + * 创建时间 + */ + @JsonProperty("createTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonProperty("updateTime") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2WithSegmentsDTO.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2WithSegmentsDTO.java new file mode 100644 index 00000000..7215f77b --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/TemplateV2WithSegmentsDTO.java @@ -0,0 +1,22 @@ +package com.ycwl.basic.integration.render.dto.template; + +import lombok.Data; + +import java.util.List; + +/** + * 模板及其片段响应DTO + */ +@Data +public class TemplateV2WithSegmentsDTO { + + /** + * 模板信息 + */ + private TemplateV2DTO template; + + /** + * 片段列表 + */ + private List segments; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/UpdateSegmentRequest.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/UpdateSegmentRequest.java new file mode 100644 index 00000000..08609a36 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/UpdateSegmentRequest.java @@ -0,0 +1,79 @@ +package com.ycwl.basic.integration.render.dto.template; + +import com.ycwl.basic.integration.render.dto.common.AudioSpecDTO; +import com.ycwl.basic.integration.render.dto.common.RenderSpecDTO; +import lombok.Data; + +import java.util.Map; + +/** + * 更新片段请求 + */ +@Data +public class UpdateSegmentRequest { + + /** + * 片段索引 + */ + private Integer segmentIndex; + + /** + * 片段类型 (RENDER, FIXED) + */ + private String segmentType; + + /** + * 是否可缓存 + */ + private Boolean cacheable; + + /** + * 素材来源类型 (ASSET, PLACEHOLDER_VIDEO, PLACEHOLDER_IMAGE, SLOT) + */ + private String sourceType; + + /** + * 素材来源引用 + */ + private String sourceRef; + + /** + * 时长(毫秒) + */ + private Long durationMs; + + /** + * 转场类型 + */ + private String transitionType; + + /** + * 转场时长(毫秒) + */ + private Integer transitionMs; + + /** + * 素材槽位键 + */ + private String slotKey; + + /** + * 条件表达式 + */ + private Map onlyIfExpr; + + /** + * 渲染参数 + */ + private RenderSpecDTO renderSpec; + + /** + * 音频参数 + */ + private AudioSpecDTO audioSpec; + + /** + * 扩展属性 + */ + private Map extendedProps; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/dto/template/UpdateTemplateRequest.java b/src/main/java/com/ycwl/basic/integration/render/dto/template/UpdateTemplateRequest.java new file mode 100644 index 00000000..fb5681c0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/dto/template/UpdateTemplateRequest.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.integration.render.dto.template; + +import com.ycwl.basic.integration.render.dto.common.OutputSpecDTO; +import lombok.Data; + +/** + * 更新模板请求 + */ +@Data +public class UpdateTemplateRequest { + + /** + * 模板名称 + */ + private String name; + + /** + * 描述 + */ + private String description; + + /** + * 缩略图URL + */ + private String thumbnailUrl; + + /** + * 默认时长(毫秒) + */ + private Long defaultDurationMs; + + /** + * 输出规格 + */ + private OutputSpecDTO outputSpec; + + /** + * 背景音乐URL + */ + private String bgmUrl; + + /** + * 背景音乐是否固定 + */ + private Boolean bgmFixed; +} diff --git a/src/main/java/com/ycwl/basic/integration/render/service/RenderJobIntegrationService.java b/src/main/java/com/ycwl/basic/integration/render/service/RenderJobIntegrationService.java new file mode 100644 index 00000000..d37652f3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/service/RenderJobIntegrationService.java @@ -0,0 +1,168 @@ +package com.ycwl.basic.integration.render.service; + +import com.ycwl.basic.integration.common.exception.IntegrationException; +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.common.response.PageResponse; +import com.ycwl.basic.integration.common.service.IntegrationFallbackService; +import com.ycwl.basic.integration.render.client.RenderJobV2Client; +import com.ycwl.basic.integration.render.dto.job.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 渲染作业集成服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RenderJobIntegrationService { + + private final RenderJobV2Client renderJobV2Client; + private final IntegrationFallbackService fallbackService; + + private static final String SERVICE_NAME = "zt-render-worker"; + + // ==================== 小程序侧接口 ==================== + + /** + * 创建预览作业(直接调用,不降级) + */ + public CreatePreviewResponse createPreview(CreatePreviewRequest request) { + log.debug("创建预览作业, templateId: {}, scenicId: {}", request.getTemplateId(), request.getScenicId()); + CommonResponse response = renderJobV2Client.createPreview(request); + return handleResponse(response, "创建预览作业失败"); + } + + /** + * 获取作业状态(带降级) + */ + public JobStatusResponse getJobStatus(Long jobId) { + log.debug("获取作业状态, jobId: {}", jobId); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "job:status:" + jobId, + () -> { + CommonResponse response = renderJobV2Client.getJobStatus(jobId); + return handleResponse(response, "获取作业状态失败"); + }, + JobStatusResponse.class + ); + } + + /** + * 获取HLS播放列表 + * 返回M3U8格式的文本内容(不降级) + */ + public String getHlsPlaylist(Long jobId) { + log.debug("获取HLS播放列表, jobId: {}", jobId); + try { + return renderJobV2Client.getHlsPlaylist(jobId); + } catch (Exception e) { + log.error("获取HLS播放列表失败, jobId: {}", jobId, e); + throw new IntegrationException(5000, "获取HLS播放列表失败: " + e.getMessage(), SERVICE_NAME); + } + } + + /** + * 获取播放列表信息(带降级) + */ + public PlaylistInfoDTO getPlaylistInfo(Long jobId) { + log.debug("获取播放列表信息, jobId: {}", jobId); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "job:playlist-info:" + jobId, + () -> { + CommonResponse response = renderJobV2Client.getPlaylistInfo(jobId); + return handleResponse(response, "获取播放列表信息失败"); + }, + PlaylistInfoDTO.class + ); + } + + /** + * 取消作业(直接调用,不降级) + */ + public void cancelJob(Long jobId) { + log.debug("取消作业, jobId: {}", jobId); + CommonResponse response = renderJobV2Client.cancelJob(jobId); + handleVoidResponse(response, "取消作业失败"); + } + + // ==================== 管理端接口 ==================== + + /** + * 获取作业列表(不降级) + */ + public PageResponse listJobs(Long scenicId, Long templateId, String status, + String jobType, Integer page, Integer pageSize) { + log.debug("查询作业列表, scenicId: {}, templateId: {}, status: {}, jobType: {}, page: {}, pageSize: {}", + scenicId, templateId, status, jobType, page, pageSize); + CommonResponse> response = + renderJobV2Client.listJobs(scenicId, templateId, status, jobType, page, pageSize); + return handleResponse(response, "查询作业列表失败"); + } + + /** + * 获取作业详情(带降级) + */ + public RenderJobV2DTO getJobDetail(Long jobId) { + log.debug("获取作业详情, jobId: {}", jobId); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "job:detail:" + jobId, + () -> { + CommonResponse response = renderJobV2Client.getJobDetail(jobId); + return handleResponse(response, "获取作业详情失败"); + }, + RenderJobV2DTO.class + ); + } + + /** + * 获取作业片段列表(带降级) + */ + @SuppressWarnings("unchecked") + public List getJobSegments(Long jobId) { + log.debug("获取作业片段列表, jobId: {}", jobId); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "job:segments:" + jobId, + () -> { + CommonResponse> response = + renderJobV2Client.getJobSegments(jobId); + return handleResponse(response, "获取作业片段列表失败"); + }, + (Class>) (Class) List.class + ); + } + + // ==================== Helper Methods ==================== + + /** + * 处理通用响应 + */ + private T handleResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.getSuccess()) { + String msg = response != null && response.getMessage() != null ? + response.getMessage() : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, SERVICE_NAME); + } + return response.getData(); + } + + /** + * 处理空响应 + */ + private void handleVoidResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.getSuccess()) { + String msg = response != null && response.getMessage() != null ? + response.getMessage() : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, SERVICE_NAME); + } + } +} diff --git a/src/main/java/com/ycwl/basic/integration/render/service/RenderTemplateIntegrationService.java b/src/main/java/com/ycwl/basic/integration/render/service/RenderTemplateIntegrationService.java new file mode 100644 index 00000000..17a6794e --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/render/service/RenderTemplateIntegrationService.java @@ -0,0 +1,207 @@ +package com.ycwl.basic.integration.render.service; + +import com.ycwl.basic.integration.common.exception.IntegrationException; +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.common.response.PageResponse; +import com.ycwl.basic.integration.common.service.IntegrationFallbackService; +import com.ycwl.basic.integration.render.client.RenderTemplateV2Client; +import com.ycwl.basic.integration.render.dto.template.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 渲染模板集成服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RenderTemplateIntegrationService { + + private final RenderTemplateV2Client renderTemplateV2Client; + private final IntegrationFallbackService fallbackService; + + private static final String SERVICE_NAME = "zt-render-worker"; + + // ==================== Template CRUD Operations ==================== + + /** + * 创建模板(直接调用,不降级) + */ + public TemplateV2DTO createTemplate(CreateTemplateRequest request) { + log.debug("创建渲染模板, scenicId: {}, name: {}", request.getScenicId(), request.getName()); + CommonResponse response = renderTemplateV2Client.createTemplate(request); + return handleResponse(response, "创建渲染模板失败"); + } + + /** + * 获取模板列表(不降级) + */ + public PageResponse listTemplates(Integer page, Integer pageSize, Long scenicId, + Integer status, String name) { + log.debug("查询渲染模板列表, page: {}, pageSize: {}, scenicId: {}, status: {}, name: {}", + page, pageSize, scenicId, status, name); + CommonResponse> response = + renderTemplateV2Client.listTemplates(page, pageSize, scenicId, status, name); + return handleResponse(response, "查询渲染模板列表失败"); + } + + /** + * 获取模板详情(带降级) + */ + public TemplateV2DTO getTemplate(Long id) { + log.debug("获取渲染模板信息, id: {}", id); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "template:" + id, + () -> { + CommonResponse response = renderTemplateV2Client.getTemplate(id); + return handleResponse(response, "获取渲染模板信息失败"); + }, + TemplateV2DTO.class + ); + } + + /** + * 获取模板及其片段(带降级) + */ + public TemplateV2WithSegmentsDTO getTemplateWithSegments(Long id) { + log.debug("获取渲染模板及片段, id: {}", id); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "template:with-segments:" + id, + () -> { + CommonResponse response = + renderTemplateV2Client.getTemplateWithSegments(id); + return handleResponse(response, "获取渲染模板及片段失败"); + }, + TemplateV2WithSegmentsDTO.class + ); + } + + /** + * 更新模板(直接调用,不降级) + */ + public void updateTemplate(Long id, UpdateTemplateRequest request) { + log.debug("更新渲染模板, id: {}, name: {}", id, request.getName()); + CommonResponse response = renderTemplateV2Client.updateTemplate(id, request); + handleVoidResponse(response, "更新渲染模板失败"); + } + + /** + * 删除模板(直接调用,不降级) + */ + public void deleteTemplate(Long id) { + log.debug("删除渲染模板, id: {}", id); + CommonResponse response = renderTemplateV2Client.deleteTemplate(id); + handleVoidResponse(response, "删除渲染模板失败"); + } + + // ==================== Template Operations ==================== + + /** + * 发布模板(直接调用,不降级) + */ + public void publishTemplate(Long id) { + log.debug("发布渲染模板, id: {}", id); + CommonResponse response = renderTemplateV2Client.publishTemplate(id); + handleVoidResponse(response, "发布渲染模板失败"); + } + + /** + * 创建新版本(直接调用,不降级) + */ + public TemplateV2DTO createTemplateVersion(Long id) { + log.debug("创建渲染模板新版本, id: {}", id); + CommonResponse response = renderTemplateV2Client.createTemplateVersion(id); + return handleResponse(response, "创建渲染模板新版本失败"); + } + + // ==================== Segment Management ==================== + + /** + * 获取模板片段列表(带降级) + */ + @SuppressWarnings("unchecked") + public List getTemplateSegments(Long id) { + log.debug("获取渲染模板片段列表, templateId: {}", id); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "template:segments:" + id, + () -> { + CommonResponse> response = + renderTemplateV2Client.getTemplateSegments(id); + return handleResponse(response, "获取渲染模板片段列表失败"); + }, + (Class>) (Class) List.class + ); + } + + /** + * 创建片段(直接调用,不降级) + */ + public TemplateV2SegmentDTO createSegment(Long templateId, CreateSegmentRequest request) { + log.debug("创建模板片段, templateId: {}, segmentIndex: {}", templateId, request.getSegmentIndex()); + CommonResponse response = + renderTemplateV2Client.createSegment(templateId, request); + return handleResponse(response, "创建模板片段失败"); + } + + /** + * 更新片段(直接调用,不降级) + */ + public void updateSegment(Long templateId, Long segmentId, UpdateSegmentRequest request) { + log.debug("更新模板片段, templateId: {}, segmentId: {}", templateId, segmentId); + CommonResponse response = + renderTemplateV2Client.updateSegment(templateId, segmentId, request); + handleVoidResponse(response, "更新模板片段失败"); + } + + /** + * 删除片段(直接调用,不降级) + */ + public void deleteSegment(Long templateId, Long segmentId) { + log.debug("删除模板片段, templateId: {}, segmentId: {}", templateId, segmentId); + CommonResponse response = renderTemplateV2Client.deleteSegment(templateId, segmentId); + handleVoidResponse(response, "删除模板片段失败"); + } + + /** + * 替换所有片段(直接调用,不降级) + */ + public void replaceSegments(Long templateId, ReplaceSegmentsRequest request) { + log.debug("替换所有模板片段, templateId: {}, segmentCount: {}", + templateId, request.getSegments() != null ? request.getSegments().size() : 0); + CommonResponse response = renderTemplateV2Client.replaceSegments(templateId, request); + handleVoidResponse(response, "替换所有模板片段失败"); + } + + // ==================== Helper Methods ==================== + + /** + * 处理通用响应 + */ + private T handleResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.getSuccess()) { + String msg = response != null && response.getMessage() != null ? + response.getMessage() : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, SERVICE_NAME); + } + return response.getData(); + } + + /** + * 处理空响应 + */ + private void handleVoidResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.getSuccess()) { + String msg = response != null && response.getMessage() != null ? + response.getMessage() : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, SERVICE_NAME); + } + } +}