You've already forked FrameTour-BE
rm template v2
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.ycwl.basic.template.model.entity.TemplateV2Entity;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Template v2 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface TemplateV2Mapper {
|
||||
List<TemplateV2Entity> list(TemplateV2ReqQuery query);
|
||||
|
||||
TemplateV2Entity getById(@Param("id") Long id);
|
||||
|
||||
int insert(TemplateV2Entity entity);
|
||||
|
||||
int updateById(TemplateV2Entity entity);
|
||||
|
||||
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||
|
||||
int softDelete(@Param("id") Long id);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Template v2 Segment Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface TemplateV2SegmentMapper {
|
||||
List<TemplateV2SegmentEntity> listByTemplateId(@Param("templateId") Long templateId);
|
||||
|
||||
int deleteByTemplateId(@Param("templateId") Long templateId);
|
||||
|
||||
int batchInsert(@Param("segments") List<TemplateV2SegmentEntity> segments);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.ycwl.basic.template.api;
|
||||
|
||||
import com.ycwl.basic.template.api.dto.RenderPlan;
|
||||
import com.ycwl.basic.template.api.dto.TemplatePlanBuildCommand;
|
||||
|
||||
/**
|
||||
* Template v2 对外能力:生成冻结 RenderPlan
|
||||
*/
|
||||
public interface ITemplateRenderPlanApi {
|
||||
RenderPlan buildRenderPlan(TemplatePlanBuildCommand command);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.ycwl.basic.template.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 冻结后的最小可执行 RenderPlan
|
||||
*/
|
||||
@Data
|
||||
public class RenderPlan {
|
||||
private Long templateId;
|
||||
private Integer templateVersion;
|
||||
|
||||
private Integer outputWidth;
|
||||
private Integer outputHeight;
|
||||
private Integer outputFps;
|
||||
private String bgmUrl;
|
||||
|
||||
private Integer totalDurationMs;
|
||||
private List<RenderPlanSegment> segments;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.ycwl.basic.template.api.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* RenderPlan 片段(已裁剪/已绑定/已计算时间轴)
|
||||
*/
|
||||
@Data
|
||||
public class RenderPlanSegment {
|
||||
private Integer templateSegmentIndex;
|
||||
private Integer planSegmentIndex;
|
||||
|
||||
private Integer startTimeMs;
|
||||
private Integer durationMs;
|
||||
|
||||
private String segmentType;
|
||||
private String sourceType;
|
||||
private String sourceRef;
|
||||
|
||||
private String boundMaterialUrl;
|
||||
|
||||
private JsonNode renderSpec;
|
||||
private JsonNode audioSpec;
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.ycwl.basic.template.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RenderPlan 生成命令
|
||||
*/
|
||||
@Data
|
||||
public class TemplatePlanBuildCommand {
|
||||
private Long scenicId;
|
||||
private Long templateId;
|
||||
/**
|
||||
* 可选:指定模板版本(用于校验模板是否被更新)
|
||||
*/
|
||||
private Integer templateVersion;
|
||||
private Long faceId;
|
||||
private Long memberId;
|
||||
|
||||
/**
|
||||
* slotKey -> 素材列表
|
||||
*/
|
||||
private Map<String, List<TemplateSlotMaterial>> materialsBySlot;
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.ycwl.basic.template.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Slot 素材引用(由调用方提供,Template v2 不直接跨模块查素材库)
|
||||
*/
|
||||
@Data
|
||||
public class TemplateSlotMaterial {
|
||||
/**
|
||||
* 素材URL(http/https)
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 创建时间(毫秒时间戳,可选,用于确定性选材)
|
||||
*/
|
||||
private Long createTimeMs;
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.ycwl.basic.template.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.template.api.ITemplateRenderPlanApi;
|
||||
import com.ycwl.basic.template.api.dto.RenderPlan;
|
||||
import com.ycwl.basic.template.api.dto.TemplatePlanBuildCommand;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2SaveReq;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2StatusUpdateReq;
|
||||
import com.ycwl.basic.template.model.resp.TemplateV2DetailResp;
|
||||
import com.ycwl.basic.template.model.resp.TemplateV2ListResp;
|
||||
import com.ycwl.basic.template.service.TemplateV2ManagementService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Template v2 管理端接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/template/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateV2Controller {
|
||||
|
||||
private final TemplateV2ManagementService templateV2ManagementService;
|
||||
private final ITemplateRenderPlanApi templateRenderPlanApi;
|
||||
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<TemplateV2ListResp>> page(@RequestBody TemplateV2ReqQuery query) {
|
||||
return templateV2ManagementService.pageQuery(query);
|
||||
}
|
||||
|
||||
@PostMapping("/list")
|
||||
public ApiResponse<List<TemplateV2ListResp>> list(@RequestBody TemplateV2ReqQuery query) {
|
||||
return templateV2ManagementService.list(query);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<TemplateV2DetailResp> detail(@PathVariable("id") Long id) {
|
||||
return templateV2ManagementService.getDetail(id);
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<Long> add(@RequestBody TemplateV2SaveReq req) {
|
||||
return templateV2ManagementService.create(req);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Boolean> update(@PathVariable("id") Long id, @RequestBody TemplateV2SaveReq req) {
|
||||
return templateV2ManagementService.update(id, req);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/status")
|
||||
public ApiResponse<Boolean> updateStatus(@PathVariable("id") Long id, @RequestBody TemplateV2StatusUpdateReq req) {
|
||||
return templateV2ManagementService.updateStatus(id, req.getStatus());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Boolean> delete(@PathVariable("id") Long id) {
|
||||
return templateV2ManagementService.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端预览:根据模板 + materialsBySlot 生成冻结 RenderPlan
|
||||
*/
|
||||
@PostMapping("/render-plan/preview")
|
||||
public ApiResponse<RenderPlan> previewRenderPlan(@RequestBody TemplatePlanBuildCommand command) {
|
||||
return ApiResponse.success(templateRenderPlanApi.buildRenderPlan(command));
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.ycwl.basic.template.enums;
|
||||
|
||||
/**
|
||||
* Template v2 片段类型
|
||||
*/
|
||||
public enum TemplateV2SegmentType {
|
||||
FIXED,
|
||||
RENDER;
|
||||
|
||||
public static TemplateV2SegmentType parse(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
for (TemplateV2SegmentType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.ycwl.basic.template.enums;
|
||||
|
||||
/**
|
||||
* Template v2 素材来源类型
|
||||
*/
|
||||
public enum TemplateV2SourceType {
|
||||
SLOT,
|
||||
FIXED_URL;
|
||||
|
||||
public static TemplateV2SourceType parse(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
for (TemplateV2SourceType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.ycwl.basic.template.model.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Template v2 主表实体(template_v2)
|
||||
*/
|
||||
@Data
|
||||
public class TemplateV2Entity {
|
||||
private Long id;
|
||||
private Long scenicId;
|
||||
private String name;
|
||||
private Integer version;
|
||||
private Integer status;
|
||||
|
||||
private Integer outputWidth;
|
||||
private Integer outputHeight;
|
||||
private Integer outputFps;
|
||||
|
||||
private String bgmUrl;
|
||||
private Integer totalDurationMs;
|
||||
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.ycwl.basic.template.model.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Template v2 片段表实体(template_v2_segment)
|
||||
*/
|
||||
@Data
|
||||
public class TemplateV2SegmentEntity {
|
||||
private Long id;
|
||||
private Long templateId;
|
||||
private Integer segmentIndex;
|
||||
|
||||
private Integer durationMs;
|
||||
private String segmentType;
|
||||
|
||||
private String sourceType;
|
||||
private String sourceRef;
|
||||
|
||||
private String onlyIfExpr;
|
||||
private String renderSpecJson;
|
||||
private String audioSpecJson;
|
||||
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.ycwl.basic.template.model.req;
|
||||
|
||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Template v2 管理端查询参数
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class TemplateV2ReqQuery extends BaseQueryParameterReq {
|
||||
private Long scenicId;
|
||||
private Integer status;
|
||||
private String name;
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.ycwl.basic.template.model.req;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Template v2 新建/更新请求
|
||||
*/
|
||||
@Data
|
||||
public class TemplateV2SaveReq {
|
||||
@NotNull(message = "scenicId不能为空")
|
||||
private Long scenicId;
|
||||
|
||||
@NotBlank(message = "name不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "outputWidth不能为空")
|
||||
@Min(value = 1, message = "outputWidth必须大于0")
|
||||
private Integer outputWidth;
|
||||
|
||||
@NotNull(message = "outputHeight不能为空")
|
||||
@Min(value = 1, message = "outputHeight必须大于0")
|
||||
private Integer outputHeight;
|
||||
|
||||
@NotNull(message = "outputFps不能为空")
|
||||
@Min(value = 1, message = "outputFps必须大于0")
|
||||
private Integer outputFps;
|
||||
|
||||
private String bgmUrl;
|
||||
private Integer status;
|
||||
|
||||
@Valid
|
||||
@NotNull(message = "segments不能为空")
|
||||
private List<TemplateV2SegmentSaveReq> segments;
|
||||
|
||||
@Data
|
||||
public static class TemplateV2SegmentSaveReq {
|
||||
@NotNull(message = "durationMs不能为空")
|
||||
@Min(value = 1, message = "durationMs必须大于0")
|
||||
private Integer durationMs;
|
||||
|
||||
@NotBlank(message = "segmentType不能为空")
|
||||
private String segmentType;
|
||||
|
||||
@NotBlank(message = "sourceType不能为空")
|
||||
private String sourceType;
|
||||
|
||||
@NotBlank(message = "sourceRef不能为空")
|
||||
private String sourceRef;
|
||||
|
||||
private JsonNode onlyIfExpr;
|
||||
private JsonNode renderSpec;
|
||||
private JsonNode audioSpec;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.ycwl.basic.template.model.req;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Template v2 启停用更新
|
||||
*/
|
||||
@Data
|
||||
public class TemplateV2StatusUpdateReq {
|
||||
@NotNull(message = "status不能为空")
|
||||
private Integer status;
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.ycwl.basic.template.model.resp;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Template v2 详情返回(含片段)
|
||||
*/
|
||||
@Data
|
||||
public class TemplateV2DetailResp {
|
||||
private Long id;
|
||||
private Long scenicId;
|
||||
private String scenicName;
|
||||
private String name;
|
||||
private Integer version;
|
||||
private Integer status;
|
||||
private Integer outputWidth;
|
||||
private Integer outputHeight;
|
||||
private Integer outputFps;
|
||||
private String bgmUrl;
|
||||
private Integer totalDurationMs;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
|
||||
private List<Segment> segments;
|
||||
|
||||
@Data
|
||||
public static class Segment {
|
||||
private Long id;
|
||||
private Integer segmentIndex;
|
||||
private Integer durationMs;
|
||||
private String segmentType;
|
||||
private String sourceType;
|
||||
private String sourceRef;
|
||||
private JsonNode onlyIfExpr;
|
||||
private JsonNode renderSpec;
|
||||
private JsonNode audioSpec;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.ycwl.basic.template.model.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Template v2 列表项返回
|
||||
*/
|
||||
@Data
|
||||
public class TemplateV2ListResp {
|
||||
private Long id;
|
||||
private Long scenicId;
|
||||
private String scenicName;
|
||||
private String name;
|
||||
private Integer version;
|
||||
private Integer status;
|
||||
private Integer totalDurationMs;
|
||||
private Date updateTime;
|
||||
}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
package com.ycwl.basic.template.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.mapper.TemplateV2Mapper;
|
||||
import com.ycwl.basic.mapper.TemplateV2SegmentMapper;
|
||||
import com.ycwl.basic.template.api.ITemplateRenderPlanApi;
|
||||
import com.ycwl.basic.template.api.dto.RenderPlan;
|
||||
import com.ycwl.basic.template.api.dto.RenderPlanSegment;
|
||||
import com.ycwl.basic.template.api.dto.TemplatePlanBuildCommand;
|
||||
import com.ycwl.basic.template.api.dto.TemplateSlotMaterial;
|
||||
import com.ycwl.basic.template.enums.TemplateV2SegmentType;
|
||||
import com.ycwl.basic.template.enums.TemplateV2SourceType;
|
||||
import com.ycwl.basic.template.model.entity.TemplateV2Entity;
|
||||
import com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity;
|
||||
import com.ycwl.basic.template.service.onlyif.OnlyIfExpression;
|
||||
import com.ycwl.basic.template.service.onlyif.OnlyIfExpressionParser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Template v2:RenderPlan 冻结服务
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateRenderPlanService implements ITemplateRenderPlanApi {
|
||||
|
||||
private static final int STATUS_ENABLED = 1;
|
||||
|
||||
private final TemplateV2Mapper templateV2Mapper;
|
||||
private final TemplateV2SegmentMapper templateV2SegmentMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final OnlyIfExpressionParser onlyIfExpressionParser;
|
||||
|
||||
@Override
|
||||
public RenderPlan buildRenderPlan(TemplatePlanBuildCommand command) {
|
||||
if (command == null || command.getTemplateId() == null) {
|
||||
throw new BizException(400, "templateId不能为空");
|
||||
}
|
||||
|
||||
TemplateV2Entity template = templateV2Mapper.getById(command.getTemplateId());
|
||||
if (template == null) {
|
||||
throw new BizException(404, "模板不存在");
|
||||
}
|
||||
if (command.getTemplateVersion() != null && !command.getTemplateVersion().equals(template.getVersion())) {
|
||||
throw new BizException(400, "模板版本不匹配");
|
||||
}
|
||||
if (template.getStatus() != null && !Objects.equals(template.getStatus(), STATUS_ENABLED)) {
|
||||
throw new BizException(400, "模板未启用");
|
||||
}
|
||||
validateOutputSpec(template);
|
||||
|
||||
List<TemplateV2SegmentEntity> templateSegments = templateV2SegmentMapper.listByTemplateId(template.getId());
|
||||
if (templateSegments == null || templateSegments.isEmpty()) {
|
||||
throw new BizException(400, "模板未配置片段");
|
||||
}
|
||||
|
||||
Map<String, List<TemplateSlotMaterial>> materialsBySlot = Objects.requireNonNullElse(command.getMaterialsBySlot(), Map.of());
|
||||
Map<String, Integer> slotCounts = buildSlotCounts(materialsBySlot);
|
||||
|
||||
List<RenderPlanSegment> planSegments = new ArrayList<>();
|
||||
int startTimeMs = 0;
|
||||
int planSegmentIndex = 0;
|
||||
for (TemplateV2SegmentEntity templateSegment : templateSegments) {
|
||||
validateSegment(templateSegment);
|
||||
if (!shouldInclude(templateSegment, slotCounts)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderPlanSegment planSegment = new RenderPlanSegment();
|
||||
planSegment.setTemplateSegmentIndex(templateSegment.getSegmentIndex());
|
||||
planSegment.setPlanSegmentIndex(planSegmentIndex++);
|
||||
planSegment.setStartTimeMs(startTimeMs);
|
||||
planSegment.setDurationMs(templateSegment.getDurationMs());
|
||||
planSegment.setSegmentType(templateSegment.getSegmentType());
|
||||
planSegment.setSourceType(templateSegment.getSourceType());
|
||||
planSegment.setSourceRef(templateSegment.getSourceRef());
|
||||
|
||||
String boundMaterialUrl = bindMaterial(templateSegment, materialsBySlot);
|
||||
planSegment.setBoundMaterialUrl(boundMaterialUrl);
|
||||
|
||||
planSegment.setRenderSpec(parseJsonRequiredOrNull(templateSegment.getRenderSpecJson(), "renderSpecJson"));
|
||||
planSegment.setAudioSpec(parseJsonRequiredOrNull(templateSegment.getAudioSpecJson(), "audioSpecJson"));
|
||||
|
||||
planSegments.add(planSegment);
|
||||
startTimeMs = Math.addExact(startTimeMs, templateSegment.getDurationMs());
|
||||
}
|
||||
|
||||
if (planSegments.isEmpty()) {
|
||||
throw new BizException(400, "RenderPlan为空:所有片段均被only_if裁剪");
|
||||
}
|
||||
|
||||
RenderPlan plan = new RenderPlan();
|
||||
plan.setTemplateId(template.getId());
|
||||
plan.setTemplateVersion(template.getVersion());
|
||||
plan.setOutputWidth(template.getOutputWidth());
|
||||
plan.setOutputHeight(template.getOutputHeight());
|
||||
plan.setOutputFps(template.getOutputFps());
|
||||
plan.setBgmUrl(template.getBgmUrl());
|
||||
plan.setTotalDurationMs(startTimeMs);
|
||||
plan.setSegments(planSegments);
|
||||
return plan;
|
||||
}
|
||||
|
||||
private void validateOutputSpec(TemplateV2Entity template) {
|
||||
if (template.getOutputWidth() == null || template.getOutputWidth() <= 0) {
|
||||
throw new BizException(400, "outputWidth非法");
|
||||
}
|
||||
if (template.getOutputHeight() == null || template.getOutputHeight() <= 0) {
|
||||
throw new BizException(400, "outputHeight非法");
|
||||
}
|
||||
if (template.getOutputFps() == null || template.getOutputFps() <= 0) {
|
||||
throw new BizException(400, "outputFps非法");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Integer> buildSlotCounts(Map<String, List<TemplateSlotMaterial>> materialsBySlot) {
|
||||
Map<String, Integer> counts = new HashMap<>();
|
||||
for (Map.Entry<String, List<TemplateSlotMaterial>> entry : materialsBySlot.entrySet()) {
|
||||
String slotKey = entry.getKey();
|
||||
List<TemplateSlotMaterial> materials = entry.getValue();
|
||||
int count = 0;
|
||||
if (materials != null) {
|
||||
for (TemplateSlotMaterial material : materials) {
|
||||
if (material != null && material.getUrl() != null && !material.getUrl().isBlank()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
counts.put(slotKey, count);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
private void validateSegment(TemplateV2SegmentEntity segment) {
|
||||
if (segment.getDurationMs() == null || segment.getDurationMs() <= 0) {
|
||||
throw new BizException(400, "片段durationMs非法:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
TemplateV2SegmentType segmentType = TemplateV2SegmentType.parse(segment.getSegmentType());
|
||||
if (segmentType == null) {
|
||||
throw new BizException(400, "片段segmentType非法:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
TemplateV2SourceType sourceType = TemplateV2SourceType.parse(segment.getSourceType());
|
||||
if (sourceType == null) {
|
||||
throw new BizException(400, "片段sourceType非法:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
if (segmentType == TemplateV2SegmentType.FIXED && sourceType != TemplateV2SourceType.FIXED_URL) {
|
||||
throw new BizException(400, "FIXED片段仅支持sourceType=FIXED_URL:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
if (segmentType == TemplateV2SegmentType.RENDER && sourceType != TemplateV2SourceType.SLOT) {
|
||||
throw new BizException(400, "RENDER片段仅支持sourceType=SLOT:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
if (segment.getSourceRef() == null || segment.getSourceRef().isBlank()) {
|
||||
throw new BizException(400, "片段sourceRef不能为空:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldInclude(TemplateV2SegmentEntity segment, Map<String, Integer> slotCounts) {
|
||||
String onlyIfExpr = segment.getOnlyIfExpr();
|
||||
if (onlyIfExpr == null || onlyIfExpr.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
JsonNode node;
|
||||
try {
|
||||
node = objectMapper.readTree(onlyIfExpr);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new BizException(400, "onlyIfExpr解析失败:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
OnlyIfExpression expr = onlyIfExpressionParser.parse(node);
|
||||
return expr.evaluate(slotCounts);
|
||||
}
|
||||
|
||||
private String bindMaterial(TemplateV2SegmentEntity segment, Map<String, List<TemplateSlotMaterial>> materialsBySlot) {
|
||||
TemplateV2SegmentType segmentType = TemplateV2SegmentType.parse(segment.getSegmentType());
|
||||
if (segmentType == TemplateV2SegmentType.FIXED) {
|
||||
if (!isHttpUrl(segment.getSourceRef())) {
|
||||
throw new BizException(400, "FIXED片段sourceRef必须是http/https URL:segmentIndex=" + segment.getSegmentIndex());
|
||||
}
|
||||
return segment.getSourceRef();
|
||||
}
|
||||
String slotKey = segment.getSourceRef();
|
||||
TemplateSlotMaterial chosen = chooseMaterial(materialsBySlot.get(slotKey));
|
||||
if (chosen == null) {
|
||||
throw new BizException(400, "缺少slot素材:" + slotKey);
|
||||
}
|
||||
if (!isHttpUrl(chosen.getUrl())) {
|
||||
throw new BizException(400, "slot素材URL必须是http/https:" + slotKey);
|
||||
}
|
||||
return chosen.getUrl();
|
||||
}
|
||||
|
||||
private TemplateSlotMaterial chooseMaterial(List<TemplateSlotMaterial> materials) {
|
||||
if (materials == null || materials.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
boolean hasCreateTime = false;
|
||||
for (TemplateSlotMaterial material : materials) {
|
||||
if (material != null && material.getCreateTimeMs() != null) {
|
||||
hasCreateTime = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasCreateTime) {
|
||||
for (TemplateSlotMaterial material : materials) {
|
||||
if (material != null && material.getUrl() != null && !material.getUrl().isBlank()) {
|
||||
return material;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
TemplateSlotMaterial best = null;
|
||||
for (TemplateSlotMaterial material : materials) {
|
||||
if (material == null || material.getUrl() == null || material.getUrl().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (best == null) {
|
||||
best = material;
|
||||
continue;
|
||||
}
|
||||
best = compareMaterial(material, best) < 0 ? material : best;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private int compareMaterial(TemplateSlotMaterial a, TemplateSlotMaterial b) {
|
||||
Long aTime = a.getCreateTimeMs();
|
||||
Long bTime = b.getCreateTimeMs();
|
||||
if (aTime == null && bTime == null) {
|
||||
return compareUrl(a.getUrl(), b.getUrl());
|
||||
}
|
||||
if (aTime == null) {
|
||||
return 1;
|
||||
}
|
||||
if (bTime == null) {
|
||||
return -1;
|
||||
}
|
||||
int timeCompare = Long.compare(bTime, aTime);
|
||||
if (timeCompare != 0) {
|
||||
return timeCompare;
|
||||
}
|
||||
return compareUrl(a.getUrl(), b.getUrl());
|
||||
}
|
||||
|
||||
private int compareUrl(String a, String b) {
|
||||
if (a == null && b == null) {
|
||||
return 0;
|
||||
}
|
||||
if (a == null) {
|
||||
return 1;
|
||||
}
|
||||
if (b == null) {
|
||||
return -1;
|
||||
}
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
private JsonNode parseJsonRequiredOrNull(String json, String fieldName) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(json);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new BizException(400, fieldName + "解析失败");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHttpUrl(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URI uri = URI.create(url);
|
||||
String scheme = uri.getScheme();
|
||||
return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.ycwl.basic.template.service;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2SaveReq;
|
||||
import com.ycwl.basic.template.model.resp.TemplateV2DetailResp;
|
||||
import com.ycwl.basic.template.model.resp.TemplateV2ListResp;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Template v2 管理端服务
|
||||
*/
|
||||
public interface TemplateV2ManagementService {
|
||||
ApiResponse<PageInfo<TemplateV2ListResp>> pageQuery(TemplateV2ReqQuery query);
|
||||
|
||||
ApiResponse<List<TemplateV2ListResp>> list(TemplateV2ReqQuery query);
|
||||
|
||||
ApiResponse<TemplateV2DetailResp> getDetail(Long templateId);
|
||||
|
||||
ApiResponse<Long> create(TemplateV2SaveReq req);
|
||||
|
||||
ApiResponse<Boolean> update(Long templateId, TemplateV2SaveReq req);
|
||||
|
||||
ApiResponse<Boolean> updateStatus(Long templateId, Integer status);
|
||||
|
||||
ApiResponse<Boolean> delete(Long templateId);
|
||||
}
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
package com.ycwl.basic.template.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.mapper.TemplateV2Mapper;
|
||||
import com.ycwl.basic.mapper.TemplateV2SegmentMapper;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.template.model.entity.TemplateV2Entity;
|
||||
import com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||
import com.ycwl.basic.template.model.req.TemplateV2SaveReq;
|
||||
import com.ycwl.basic.template.model.resp.TemplateV2DetailResp;
|
||||
import com.ycwl.basic.template.model.resp.TemplateV2ListResp;
|
||||
import com.ycwl.basic.template.service.TemplateV2ManagementService;
|
||||
import com.ycwl.basic.template.service.onlyif.OnlyIfExpressionParser;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.ycwl.basic.template.enums.TemplateV2SegmentType;
|
||||
import com.ycwl.basic.template.enums.TemplateV2SourceType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import java.net.URI;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateV2ManagementServiceImpl implements TemplateV2ManagementService {
|
||||
|
||||
private static final int STATUS_ENABLED = 1;
|
||||
private static final int STATUS_DISABLED = 0;
|
||||
|
||||
private final TemplateV2Mapper templateV2Mapper;
|
||||
private final TemplateV2SegmentMapper templateV2SegmentMapper;
|
||||
private final ScenicRepository scenicRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final OnlyIfExpressionParser onlyIfExpressionParser;
|
||||
|
||||
@Override
|
||||
public ApiResponse<PageInfo<TemplateV2ListResp>> pageQuery(TemplateV2ReqQuery query) {
|
||||
PageHelper.startPage(query.getPageNum(), query.getPageSize());
|
||||
List<TemplateV2Entity> entities = templateV2Mapper.list(query);
|
||||
List<TemplateV2ListResp> items = toListResp(entities);
|
||||
return ApiResponse.success(new PageInfo<>(items));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<List<TemplateV2ListResp>> list(TemplateV2ReqQuery query) {
|
||||
List<TemplateV2Entity> entities = templateV2Mapper.list(query);
|
||||
return ApiResponse.success(toListResp(entities));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<TemplateV2DetailResp> getDetail(Long templateId) {
|
||||
TemplateV2Entity template = templateV2Mapper.getById(templateId);
|
||||
if (template == null) {
|
||||
return ApiResponse.fail("模板不存在");
|
||||
}
|
||||
List<TemplateV2SegmentEntity> segments = templateV2SegmentMapper.listByTemplateId(templateId);
|
||||
TemplateV2DetailResp resp = toDetailResp(template, segments);
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ApiResponse<Long> create(TemplateV2SaveReq req) {
|
||||
validateSaveReq(req);
|
||||
|
||||
long templateId = SnowFlakeUtil.getLongId();
|
||||
int totalDurationMs = sumDurationMs(req.getSegments());
|
||||
|
||||
TemplateV2Entity entity = new TemplateV2Entity();
|
||||
entity.setId(templateId);
|
||||
entity.setScenicId(req.getScenicId());
|
||||
entity.setName(req.getName());
|
||||
entity.setVersion(1);
|
||||
entity.setStatus(normalizeStatus(req.getStatus()));
|
||||
entity.setOutputWidth(req.getOutputWidth());
|
||||
entity.setOutputHeight(req.getOutputHeight());
|
||||
entity.setOutputFps(req.getOutputFps());
|
||||
entity.setBgmUrl(req.getBgmUrl());
|
||||
entity.setTotalDurationMs(totalDurationMs);
|
||||
templateV2Mapper.insert(entity);
|
||||
|
||||
saveSegments(templateId, req.getSegments());
|
||||
return ApiResponse.success(templateId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ApiResponse<Boolean> update(Long templateId, TemplateV2SaveReq req) {
|
||||
validateSaveReq(req);
|
||||
|
||||
TemplateV2Entity old = templateV2Mapper.getById(templateId);
|
||||
if (old == null) {
|
||||
return ApiResponse.fail("模板不存在");
|
||||
}
|
||||
|
||||
int totalDurationMs = sumDurationMs(req.getSegments());
|
||||
|
||||
TemplateV2Entity entity = new TemplateV2Entity();
|
||||
entity.setId(templateId);
|
||||
entity.setScenicId(req.getScenicId());
|
||||
entity.setName(req.getName());
|
||||
entity.setVersion(old.getVersion() == null ? 1 : old.getVersion() + 1);
|
||||
entity.setStatus(req.getStatus() == null ? normalizeStatus(old.getStatus()) : normalizeStatus(req.getStatus()));
|
||||
entity.setOutputWidth(req.getOutputWidth());
|
||||
entity.setOutputHeight(req.getOutputHeight());
|
||||
entity.setOutputFps(req.getOutputFps());
|
||||
entity.setBgmUrl(req.getBgmUrl());
|
||||
entity.setTotalDurationMs(totalDurationMs);
|
||||
templateV2Mapper.updateById(entity);
|
||||
|
||||
templateV2SegmentMapper.deleteByTemplateId(templateId);
|
||||
saveSegments(templateId, req.getSegments());
|
||||
return ApiResponse.success(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<Boolean> updateStatus(Long templateId, Integer status) {
|
||||
Integer normalized = normalizeStatus(status);
|
||||
int updated = templateV2Mapper.updateStatus(templateId, normalized);
|
||||
if (updated <= 0) {
|
||||
return ApiResponse.fail("更新失败:模板不存在或已删除");
|
||||
}
|
||||
return ApiResponse.success(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<Boolean> delete(Long templateId) {
|
||||
int updated = templateV2Mapper.softDelete(templateId);
|
||||
if (updated <= 0) {
|
||||
return ApiResponse.fail("删除失败:模板不存在或已删除");
|
||||
}
|
||||
return ApiResponse.success(true);
|
||||
}
|
||||
|
||||
private List<TemplateV2ListResp> toListResp(List<TemplateV2Entity> entities) {
|
||||
if (entities == null || entities.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Long> scenicIds = entities.stream()
|
||||
.map(TemplateV2Entity::getScenicId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
||||
|
||||
return entities.stream().map(item -> {
|
||||
TemplateV2ListResp resp = new TemplateV2ListResp();
|
||||
resp.setId(item.getId());
|
||||
resp.setScenicId(item.getScenicId());
|
||||
resp.setScenicName(item.getScenicId() == null ? null : scenicNames.get(item.getScenicId()));
|
||||
resp.setName(item.getName());
|
||||
resp.setVersion(item.getVersion());
|
||||
resp.setStatus(item.getStatus());
|
||||
resp.setTotalDurationMs(item.getTotalDurationMs());
|
||||
resp.setUpdateTime(item.getUpdateTime());
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private TemplateV2DetailResp toDetailResp(TemplateV2Entity template, List<TemplateV2SegmentEntity> segments) {
|
||||
TemplateV2DetailResp resp = new TemplateV2DetailResp();
|
||||
resp.setId(template.getId());
|
||||
resp.setScenicId(template.getScenicId());
|
||||
if (template.getScenicId() != null) {
|
||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(List.of(template.getScenicId()));
|
||||
resp.setScenicName(scenicNames.get(template.getScenicId()));
|
||||
}
|
||||
resp.setName(template.getName());
|
||||
resp.setVersion(template.getVersion());
|
||||
resp.setStatus(template.getStatus());
|
||||
resp.setOutputWidth(template.getOutputWidth());
|
||||
resp.setOutputHeight(template.getOutputHeight());
|
||||
resp.setOutputFps(template.getOutputFps());
|
||||
resp.setBgmUrl(template.getBgmUrl());
|
||||
resp.setTotalDurationMs(template.getTotalDurationMs());
|
||||
resp.setCreateTime(template.getCreateTime());
|
||||
resp.setUpdateTime(template.getUpdateTime());
|
||||
|
||||
List<TemplateV2DetailResp.Segment> segmentList = new ArrayList<>();
|
||||
for (TemplateV2SegmentEntity segment : Objects.requireNonNullElse(segments, List.<TemplateV2SegmentEntity>of())) {
|
||||
TemplateV2DetailResp.Segment seg = new TemplateV2DetailResp.Segment();
|
||||
seg.setId(segment.getId());
|
||||
seg.setSegmentIndex(segment.getSegmentIndex());
|
||||
seg.setDurationMs(segment.getDurationMs());
|
||||
seg.setSegmentType(segment.getSegmentType());
|
||||
seg.setSourceType(segment.getSourceType());
|
||||
seg.setSourceRef(segment.getSourceRef());
|
||||
seg.setOnlyIfExpr(parseJsonOrNull(segment.getOnlyIfExpr()));
|
||||
seg.setRenderSpec(parseJsonOrNull(segment.getRenderSpecJson()));
|
||||
seg.setAudioSpec(parseJsonOrNull(segment.getAudioSpecJson()));
|
||||
segmentList.add(seg);
|
||||
}
|
||||
resp.setSegments(segmentList);
|
||||
return resp;
|
||||
}
|
||||
|
||||
private void validateSaveReq(TemplateV2SaveReq req) {
|
||||
if (req.getSegments() == null || req.getSegments().isEmpty()) {
|
||||
throw new BizException(400, "segments不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private int normalizeStatus(Integer status) {
|
||||
if (status == null) {
|
||||
return STATUS_ENABLED;
|
||||
}
|
||||
if (status == STATUS_ENABLED || status == STATUS_DISABLED) {
|
||||
return status;
|
||||
}
|
||||
throw new BizException(400, "status仅支持0/1");
|
||||
}
|
||||
|
||||
private int sumDurationMs(List<TemplateV2SaveReq.TemplateV2SegmentSaveReq> segments) {
|
||||
int total = 0;
|
||||
for (TemplateV2SaveReq.TemplateV2SegmentSaveReq item : segments) {
|
||||
total = Math.addExact(total, item.getDurationMs());
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private void saveSegments(Long templateId, List<TemplateV2SaveReq.TemplateV2SegmentSaveReq> segments) {
|
||||
List<TemplateV2SegmentEntity> entities = new ArrayList<>();
|
||||
for (int index = 0; index < segments.size(); index++) {
|
||||
TemplateV2SaveReq.TemplateV2SegmentSaveReq req = segments.get(index);
|
||||
validateOnlyIfExpr(req.getOnlyIfExpr());
|
||||
validateSegmentDefinition(req);
|
||||
|
||||
TemplateV2SegmentEntity entity = new TemplateV2SegmentEntity();
|
||||
entity.setId(SnowFlakeUtil.getLongId());
|
||||
entity.setTemplateId(templateId);
|
||||
entity.setSegmentIndex(index);
|
||||
entity.setDurationMs(req.getDurationMs());
|
||||
entity.setSegmentType(req.getSegmentType());
|
||||
entity.setSourceType(req.getSourceType());
|
||||
entity.setSourceRef(req.getSourceRef());
|
||||
entity.setOnlyIfExpr(writeJsonOrNull(req.getOnlyIfExpr()));
|
||||
entity.setRenderSpecJson(writeJsonOrNull(req.getRenderSpec()));
|
||||
entity.setAudioSpecJson(writeJsonOrNull(req.getAudioSpec()));
|
||||
entities.add(entity);
|
||||
}
|
||||
if (!entities.isEmpty()) {
|
||||
templateV2SegmentMapper.batchInsert(entities);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSegmentDefinition(TemplateV2SaveReq.TemplateV2SegmentSaveReq req) {
|
||||
TemplateV2SegmentType segmentType = TemplateV2SegmentType.parse(req.getSegmentType());
|
||||
if (segmentType == null) {
|
||||
throw new BizException(400, "segmentType仅支持FIXED/RENDER");
|
||||
}
|
||||
TemplateV2SourceType sourceType = TemplateV2SourceType.parse(req.getSourceType());
|
||||
if (sourceType == null) {
|
||||
throw new BizException(400, "sourceType仅支持SLOT/FIXED_URL");
|
||||
}
|
||||
if (segmentType == TemplateV2SegmentType.FIXED && sourceType != TemplateV2SourceType.FIXED_URL) {
|
||||
throw new BizException(400, "FIXED片段仅支持sourceType=FIXED_URL");
|
||||
}
|
||||
if (segmentType == TemplateV2SegmentType.RENDER && sourceType != TemplateV2SourceType.SLOT) {
|
||||
throw new BizException(400, "RENDER片段仅支持sourceType=SLOT");
|
||||
}
|
||||
if (sourceType == TemplateV2SourceType.FIXED_URL && !isHttpUrl(req.getSourceRef())) {
|
||||
throw new BizException(400, "sourceRef必须是http/https URL");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOnlyIfExpr(JsonNode onlyIfExpr) {
|
||||
if (onlyIfExpr == null || onlyIfExpr.isNull()) {
|
||||
return;
|
||||
}
|
||||
onlyIfExpressionParser.parse(onlyIfExpr);
|
||||
}
|
||||
|
||||
private String writeJsonOrNull(JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(node);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new BizException(400, "JSON序列化失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode parseJsonOrNull(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(json);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHttpUrl(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URI uri = URI.create(url);
|
||||
String scheme = uri.getScheme();
|
||||
return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AndExpression implements OnlyIfExpression {
|
||||
private final List<OnlyIfExpression> items;
|
||||
|
||||
public AndExpression(List<OnlyIfExpression> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||
for (OnlyIfExpression item : items) {
|
||||
if (!item.evaluate(slotCounts)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<OnlyIfExpression> getItems() {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class CountGteExpression implements OnlyIfExpression {
|
||||
private final String slotKey;
|
||||
private final int value;
|
||||
|
||||
public CountGteExpression(String slotKey, int value) {
|
||||
this.slotKey = slotKey;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||
if (slotCounts == null) {
|
||||
return false;
|
||||
}
|
||||
return slotCounts.getOrDefault(slotKey, 0) >= value;
|
||||
}
|
||||
|
||||
public String getSlotKey() {
|
||||
return slotKey;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ExistsExpression implements OnlyIfExpression {
|
||||
private final String slotKey;
|
||||
|
||||
public ExistsExpression(String slotKey) {
|
||||
this.slotKey = slotKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||
if (slotCounts == null) {
|
||||
return false;
|
||||
}
|
||||
return slotCounts.getOrDefault(slotKey, 0) > 0;
|
||||
}
|
||||
|
||||
public String getSlotKey() {
|
||||
return slotKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NotExpression implements OnlyIfExpression {
|
||||
private final OnlyIfExpression expr;
|
||||
|
||||
public NotExpression(OnlyIfExpression expr) {
|
||||
this.expr = expr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||
return !expr.evaluate(slotCounts);
|
||||
}
|
||||
|
||||
public OnlyIfExpression getExpr() {
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* only_if 受限表达式(基于 slotKey 的数量判断)
|
||||
*/
|
||||
public interface OnlyIfExpression {
|
||||
boolean evaluate(Map<String, Integer> slotCounts);
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* only_if 表达式解析器(JSON AST):
|
||||
* <pre>
|
||||
* {"op":"exists","slotKey":"device:123"}
|
||||
* {"op":"count_gte","slotKey":"P:123","value":2}
|
||||
* {"op":"and","items":[...]}
|
||||
* {"op":"or","items":[...]}
|
||||
* {"op":"not","expr":{...}}
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
public class OnlyIfExpressionParser {
|
||||
|
||||
public OnlyIfExpression parse(JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (!node.isObject()) {
|
||||
throw new BizException(400, "onlyIfExpr必须是JSON对象");
|
||||
}
|
||||
String op = text(node, "op");
|
||||
if (op == null) {
|
||||
op = text(node, "type");
|
||||
}
|
||||
if (op == null) {
|
||||
throw new BizException(400, "onlyIfExpr缺少op字段");
|
||||
}
|
||||
|
||||
return switch (op.toLowerCase()) {
|
||||
case "exists" -> new ExistsExpression(requiredText(node, "slotKey"));
|
||||
case "count_gte" -> new CountGteExpression(requiredText(node, "slotKey"), requiredInt(node, "value", "n"));
|
||||
case "and" -> new AndExpression(parseItems(node));
|
||||
case "or" -> new OrExpression(parseItems(node));
|
||||
case "not" -> new NotExpression(parse(requiredNode(node, "expr", "item")));
|
||||
default -> throw new BizException(400, "onlyIfExpr不支持的op: " + op);
|
||||
};
|
||||
}
|
||||
|
||||
private List<OnlyIfExpression> parseItems(JsonNode node) {
|
||||
JsonNode itemsNode = node.get("items");
|
||||
if (itemsNode == null) {
|
||||
itemsNode = node.get("children");
|
||||
}
|
||||
if (itemsNode == null || !itemsNode.isArray() || itemsNode.isEmpty()) {
|
||||
throw new BizException(400, "onlyIfExpr的items必须是非空数组");
|
||||
}
|
||||
List<OnlyIfExpression> items = new ArrayList<>();
|
||||
for (JsonNode item : itemsNode) {
|
||||
items.add(parse(item));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private String requiredText(JsonNode node, String fieldName) {
|
||||
String value = text(node, fieldName);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new BizException(400, "onlyIfExpr缺少字段: " + fieldName);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private int requiredInt(JsonNode node, String... fieldNames) {
|
||||
for (String fieldName : fieldNames) {
|
||||
JsonNode valueNode = node.get(fieldName);
|
||||
if (valueNode != null && valueNode.isNumber()) {
|
||||
int value = valueNode.asInt();
|
||||
if (value < 0) {
|
||||
throw new BizException(400, "onlyIfExpr字段" + fieldName + "不能为负数");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw new BizException(400, "onlyIfExpr缺少字段: value");
|
||||
}
|
||||
|
||||
private JsonNode requiredNode(JsonNode node, String... fieldNames) {
|
||||
for (String fieldName : fieldNames) {
|
||||
JsonNode valueNode = node.get(fieldName);
|
||||
if (valueNode != null && !valueNode.isNull()) {
|
||||
return valueNode;
|
||||
}
|
||||
}
|
||||
throw new BizException(400, "onlyIfExpr缺少字段: expr");
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String fieldName) {
|
||||
JsonNode valueNode = node.get(fieldName);
|
||||
if (valueNode == null || valueNode.isNull() || !valueNode.isTextual()) {
|
||||
return null;
|
||||
}
|
||||
return valueNode.asText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.ycwl.basic.template.service.onlyif;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class OrExpression implements OnlyIfExpression {
|
||||
private final List<OnlyIfExpression> items;
|
||||
|
||||
public OrExpression(List<OnlyIfExpression> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||
for (OnlyIfExpression item : items) {
|
||||
if (item.evaluate(slotCounts)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<OnlyIfExpression> getItems() {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<?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.ycwl.basic.mapper.TemplateV2Mapper">
|
||||
<select id="list" resultType="com.ycwl.basic.template.model.entity.TemplateV2Entity">
|
||||
select *
|
||||
from template_v2
|
||||
<where>
|
||||
deleted = 0
|
||||
<if test="scenicId != null">
|
||||
and scenic_id = #{scenicId}
|
||||
</if>
|
||||
<if test="status != null">
|
||||
and status = #{status}
|
||||
</if>
|
||||
<if test="name != null and name != ''">
|
||||
and locate(#{name}, `name`) > 0
|
||||
</if>
|
||||
</where>
|
||||
order by update_time desc, id desc
|
||||
</select>
|
||||
|
||||
<select id="getById" resultType="com.ycwl.basic.template.model.entity.TemplateV2Entity">
|
||||
select *
|
||||
from template_v2
|
||||
where id = #{id} and deleted = 0
|
||||
</select>
|
||||
|
||||
<insert id="insert">
|
||||
insert into template_v2(
|
||||
id, scenic_id, `name`, version, status,
|
||||
output_width, output_height, output_fps,
|
||||
bgm_url, total_duration_ms,
|
||||
deleted, create_time, update_time
|
||||
)
|
||||
values (
|
||||
#{id}, #{scenicId}, #{name}, #{version}, #{status},
|
||||
#{outputWidth}, #{outputHeight}, #{outputFps},
|
||||
#{bgmUrl}, #{totalDurationMs},
|
||||
0, now(), now()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateById">
|
||||
update template_v2
|
||||
<set>
|
||||
update_time = now(),
|
||||
<if test="scenicId != null">scenic_id = #{scenicId},</if>
|
||||
<if test="name != null">`name` = #{name},</if>
|
||||
<if test="version != null">version = #{version},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="outputWidth != null">output_width = #{outputWidth},</if>
|
||||
<if test="outputHeight != null">output_height = #{outputHeight},</if>
|
||||
<if test="outputFps != null">output_fps = #{outputFps},</if>
|
||||
<if test="bgmUrl != null">bgm_url = #{bgmUrl},</if>
|
||||
<if test="totalDurationMs != null">total_duration_ms = #{totalDurationMs},</if>
|
||||
</set>
|
||||
where id = #{id} and deleted = 0
|
||||
</update>
|
||||
|
||||
<update id="updateStatus">
|
||||
update template_v2
|
||||
set status = #{status}, update_time = now()
|
||||
where id = #{id} and deleted = 0
|
||||
</update>
|
||||
|
||||
<update id="softDelete">
|
||||
update template_v2
|
||||
set deleted = 1, update_time = now()
|
||||
where id = #{id} and deleted = 0
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?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.ycwl.basic.mapper.TemplateV2SegmentMapper">
|
||||
<select id="listByTemplateId" resultType="com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity">
|
||||
select *
|
||||
from template_v2_segment
|
||||
where template_id = #{templateId}
|
||||
order by segment_index
|
||||
</select>
|
||||
|
||||
<delete id="deleteByTemplateId">
|
||||
delete from template_v2_segment where template_id = #{templateId}
|
||||
</delete>
|
||||
|
||||
<insert id="batchInsert">
|
||||
insert into template_v2_segment(
|
||||
id, template_id, segment_index,
|
||||
duration_ms, segment_type,
|
||||
source_type, source_ref,
|
||||
only_if_expr, render_spec_json, audio_spec_json,
|
||||
create_time, update_time
|
||||
)
|
||||
values
|
||||
<foreach collection="segments" item="item" separator=",">
|
||||
(
|
||||
#{item.id}, #{item.templateId}, #{item.segmentIndex},
|
||||
#{item.durationMs}, #{item.segmentType},
|
||||
#{item.sourceType}, #{item.sourceRef},
|
||||
#{item.onlyIfExpr}, #{item.renderSpecJson}, #{item.audioSpecJson},
|
||||
now(), now()
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user