You've already forked FrameTour-BE
feat(puzzle): 实现拼图自动填充规则引擎及相关功能
- 新增拼图填充规则管理Controller、DTO、Entity等核心类 - 实现条件评估策略模式,支持多种匹配规则 - 实现数据源解析策略模式,支持多种数据来源 - 新增拼图元素自动填充引擎,支持优先级匹配和动态填充 - 在SourceMapper中增加设备统计和查询相关方法 - 在PuzzleGenerateRequest中新增faceId字段用于触发自动填充 - 完善相关枚举类和工具类,提升系统可维护性和扩展性
This commit is contained in:
@@ -107,4 +107,28 @@ public interface SourceMapper {
|
||||
int addFromZTSource(SourceEntity source);
|
||||
|
||||
SourceEntity getBySampleIdAndType(Long faceSampleId, Integer type);
|
||||
|
||||
/**
|
||||
* 统计faceId关联的不同设备数量
|
||||
* @param faceId 人脸ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer countDistinctDevicesByFaceId(Long faceId);
|
||||
|
||||
/**
|
||||
* 根据faceId和设备索引获取source
|
||||
* @param faceId 人脸ID
|
||||
* @param deviceIndex 设备索引(从0开始)
|
||||
* @param type 素材类型(1-视频,2-图片)
|
||||
* @param sortStrategy 排序策略
|
||||
* @return source实体
|
||||
*/
|
||||
SourceEntity getSourceByFaceAndDeviceIndex(Long faceId, Integer deviceIndex, Integer type, String sortStrategy);
|
||||
|
||||
/**
|
||||
* 获取faceId关联的所有设备ID列表
|
||||
* @param faceId 人脸ID
|
||||
* @return 设备ID列表
|
||||
*/
|
||||
List<Long> getDeviceIdsByFaceId(Long faceId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.ycwl.basic.puzzle.controller;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleSaveRequest;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleFillRuleService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图填充规则管理Controller
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/puzzle/admin/fill-rule")
|
||||
@RequiredArgsConstructor
|
||||
public class PuzzleFillRuleController {
|
||||
|
||||
private final IPuzzleFillRuleService fillRuleService;
|
||||
|
||||
/**
|
||||
* 创建填充规则
|
||||
*
|
||||
* @param request 保存请求(包含主规则+明细列表)
|
||||
* @return 规则ID
|
||||
*/
|
||||
@PostMapping
|
||||
public ApiResponse<Long> create(@RequestBody PuzzleFillRuleSaveRequest request) {
|
||||
log.info("创建填充规则, ruleName={}, itemCount={}",
|
||||
request.getRuleName(),
|
||||
request.getItems() != null ? request.getItems().size() : 0);
|
||||
|
||||
Long ruleId = fillRuleService.create(request);
|
||||
return ApiResponse.success(ruleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新填充规则
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @param request 保存请求
|
||||
* @return 是否成功
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Boolean> update(@PathVariable Long id,
|
||||
@RequestBody PuzzleFillRuleSaveRequest request) {
|
||||
log.info("更新填充规则, ruleId={}, ruleName={}", id, request.getRuleName());
|
||||
|
||||
request.setId(id);
|
||||
Boolean success = fillRuleService.update(request);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除填充规则
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
log.info("删除填充规则, ruleId={}", id);
|
||||
|
||||
Boolean success = fillRuleService.delete(id);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条规则(含明细)
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @return 规则DTO
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<PuzzleFillRuleDTO> getById(@PathVariable Long id) {
|
||||
log.info("查询填充规则, ruleId={}", id);
|
||||
|
||||
PuzzleFillRuleDTO rule = fillRuleService.getById(id);
|
||||
return ApiResponse.success(rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模板的所有规则(含明细)
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
@GetMapping("/template/{templateId}")
|
||||
public ApiResponse<List<PuzzleFillRuleDTO>> listByTemplateId(@PathVariable Long templateId) {
|
||||
log.info("查询模板的所有填充规则, templateId={}", templateId);
|
||||
|
||||
List<PuzzleFillRuleDTO> rules = fillRuleService.listByTemplateId(templateId);
|
||||
return ApiResponse.success(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用规则
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @param enabled 是否启用(0-禁用 1-启用)
|
||||
* @return 是否成功
|
||||
*/
|
||||
@PostMapping("/{id}/toggle")
|
||||
public ApiResponse<Boolean> toggleEnabled(@PathVariable Long id,
|
||||
@RequestParam Integer enabled) {
|
||||
log.info("切换规则启用状态, ruleId={}, enabled={}", id, enabled);
|
||||
|
||||
Boolean success = fillRuleService.toggleEnabled(id, enabled);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图填充规则DTO
|
||||
*/
|
||||
@Data
|
||||
public class PuzzleFillRuleDTO {
|
||||
|
||||
/**
|
||||
* 规则ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 条件类型
|
||||
*/
|
||||
private String conditionType;
|
||||
|
||||
/**
|
||||
* 条件值(JSON字符串)
|
||||
*/
|
||||
private String conditionValue;
|
||||
|
||||
/**
|
||||
* 优先级
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Integer enabled;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 规则描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 明细列表
|
||||
*/
|
||||
private List<PuzzleFillRuleItemDTO> items;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 拼图填充规则明细DTO
|
||||
*/
|
||||
@Data
|
||||
public class PuzzleFillRuleItemDTO {
|
||||
|
||||
/**
|
||||
* 明细ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 目标元素标识
|
||||
*/
|
||||
private String elementKey;
|
||||
|
||||
/**
|
||||
* 数据源类型
|
||||
*/
|
||||
private String dataSource;
|
||||
|
||||
/**
|
||||
* 数据过滤条件(JSON字符串)
|
||||
*/
|
||||
private String sourceFilter;
|
||||
|
||||
/**
|
||||
* 排序策略
|
||||
*/
|
||||
private String sortStrategy;
|
||||
|
||||
/**
|
||||
* 降级默认值
|
||||
*/
|
||||
private String fallbackValue;
|
||||
|
||||
/**
|
||||
* 明细排序
|
||||
*/
|
||||
private Integer itemOrder;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图填充规则保存请求
|
||||
* 包含主规则+明细列表
|
||||
*/
|
||||
@Data
|
||||
public class PuzzleFillRuleSaveRequest {
|
||||
|
||||
/**
|
||||
* 规则ID(更新时传入)
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 条件类型
|
||||
*/
|
||||
private String conditionType;
|
||||
|
||||
/**
|
||||
* 条件值(JSON字符串)
|
||||
*/
|
||||
private String conditionValue;
|
||||
|
||||
/**
|
||||
* 优先级
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Integer enabled;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 规则描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 明细列表(主从一起保存)
|
||||
*/
|
||||
private List<PuzzleFillRuleItemDTO> items;
|
||||
}
|
||||
@@ -38,9 +38,15 @@ public class PuzzleGenerateRequest {
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 人脸ID(可选,用于触发自动填充规则)
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 动态数据(key为元素的elementKey,value为实际值)
|
||||
* 例如:{"userAvatar": "https://...", "userName": "张三", "orderNumber": "ORDER123"}
|
||||
* 注意:手动传入的dynamicData优先级高于自动填充的数据
|
||||
*/
|
||||
private Map<String, String> dynamicData;
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ycwl.basic.puzzle.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 拼图自动填充规则实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("puzzle_fill_rule")
|
||||
public class PuzzleFillRuleEntity {
|
||||
|
||||
/**
|
||||
* 规则ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 条件类型: DEVICE_COUNT-机位数量
|
||||
*/
|
||||
private String conditionType;
|
||||
|
||||
/**
|
||||
* 条件值(JSON格式)
|
||||
*/
|
||||
private String conditionValue;
|
||||
|
||||
/**
|
||||
* 优先级(数值越大越优先)
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 是否启用(0-否 1-是)
|
||||
*/
|
||||
private Integer enabled;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 规则描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 删除标记(0-未删除 1-已删除)
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ycwl.basic.puzzle.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 拼图自动填充规则明细实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("puzzle_fill_rule_item")
|
||||
public class PuzzleFillRuleItemEntity {
|
||||
|
||||
/**
|
||||
* 明细ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的规则ID
|
||||
*/
|
||||
private Long ruleId;
|
||||
|
||||
/**
|
||||
* 目标元素标识
|
||||
*/
|
||||
private String elementKey;
|
||||
|
||||
/**
|
||||
* 数据源类型: FACE_URL, SOURCE_IMAGE, DEVICE_IMAGE等
|
||||
*/
|
||||
private String dataSource;
|
||||
|
||||
/**
|
||||
* 数据过滤条件(JSON格式)
|
||||
*/
|
||||
private String sourceFilter;
|
||||
|
||||
/**
|
||||
* 排序策略: LATEST-最新, SCORE_DESC-分数降序
|
||||
*/
|
||||
private String sortStrategy;
|
||||
|
||||
/**
|
||||
* 降级默认值
|
||||
*/
|
||||
private String fallbackValue;
|
||||
|
||||
/**
|
||||
* 明细排序
|
||||
*/
|
||||
private Integer itemOrder;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.ycwl.basic.puzzle.fill;
|
||||
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
|
||||
import com.ycwl.basic.puzzle.fill.condition.ConditionContext;
|
||||
import com.ycwl.basic.puzzle.fill.condition.ConditionEvaluator;
|
||||
import com.ycwl.basic.puzzle.fill.datasource.DataSourceContext;
|
||||
import com.ycwl.basic.puzzle.fill.datasource.DataSourceResolver;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 拼图元素自动填充引擎
|
||||
* 核心业务逻辑:
|
||||
* 1. 查询模板的所有规则(按priority DESC)
|
||||
* 2. 遍历规则,评估条件是否匹配
|
||||
* 3. 匹配成功后,批量填充该规则的所有明细项
|
||||
* 4. 匹配第一条后停止(优先级逻辑)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PuzzleElementFillEngine {
|
||||
|
||||
@Autowired
|
||||
private PuzzleFillRuleMapper ruleMapper;
|
||||
|
||||
@Autowired
|
||||
private PuzzleFillRuleItemMapper itemMapper;
|
||||
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
@Autowired
|
||||
private ConditionEvaluator conditionEvaluator;
|
||||
|
||||
@Autowired
|
||||
private DataSourceResolver dataSourceResolver;
|
||||
|
||||
/**
|
||||
* 执行填充规则
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @param faceId 人脸ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 填充后的dynamicData
|
||||
*/
|
||||
public Map<String, String> execute(Long templateId, Long faceId, Long scenicId) {
|
||||
Map<String, String> dynamicData = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 1. 查询模板的所有启用规则(按priority DESC排序)
|
||||
List<PuzzleFillRuleEntity> rules = ruleMapper.listByTemplateAndScenic(templateId, scenicId);
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
log.debug("模板[{}]没有配置自动填充规则", templateId);
|
||||
return dynamicData;
|
||||
}
|
||||
|
||||
log.info("模板[{}]共有{}条填充规则,开始执行...", templateId, rules.size());
|
||||
|
||||
// 2. 统计机位数量和获取机位列表(缓存,避免重复查询)
|
||||
Integer deviceCount = sourceMapper.countDistinctDevicesByFaceId(faceId);
|
||||
List<Long> deviceIds = sourceMapper.getDeviceIdsByFaceId(faceId);
|
||||
log.debug("faceId[{}]关联机位数量: {}, 机位列表: {}", faceId, deviceCount, deviceIds);
|
||||
|
||||
// 3. 构建条件评估上下文
|
||||
ConditionContext conditionContext = ConditionContext.builder()
|
||||
.faceId(faceId)
|
||||
.scenicId(scenicId)
|
||||
.deviceCount(deviceCount)
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
// 4. 遍历规则,匹配第一条后停止
|
||||
for (PuzzleFillRuleEntity rule : rules) {
|
||||
// 评估条件是否匹配
|
||||
boolean matched = conditionEvaluator.evaluate(rule, conditionContext);
|
||||
|
||||
if (!matched) {
|
||||
log.debug("规则[{}]条件不匹配,跳过", rule.getRuleName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 条件匹配!查询该规则的所有明细
|
||||
log.info("规则[{}]匹配成功,开始执行填充...", rule.getRuleName());
|
||||
List<PuzzleFillRuleItemEntity> items = itemMapper.listByRuleId(rule.getId());
|
||||
|
||||
if (items == null || items.isEmpty()) {
|
||||
log.warn("规则[{}]没有配置明细项", rule.getRuleName());
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. 批量填充dynamicData
|
||||
DataSourceContext dataSourceContext = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.scenicId(scenicId)
|
||||
.build();
|
||||
|
||||
int successCount = 0;
|
||||
for (PuzzleFillRuleItemEntity item : items) {
|
||||
String value = dataSourceResolver.resolve(
|
||||
item.getDataSource(),
|
||||
item.getSourceFilter(),
|
||||
item.getSortStrategy(),
|
||||
item.getFallbackValue(),
|
||||
dataSourceContext
|
||||
);
|
||||
|
||||
if (value != null && !value.isEmpty()) {
|
||||
dynamicData.put(item.getElementKey(), value);
|
||||
successCount++;
|
||||
log.debug("填充成功: {} -> {}", item.getElementKey(), value);
|
||||
} else {
|
||||
log.debug("填充失败(值为空): {}", item.getElementKey());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("规则[{}]执行完成,成功填充{}/{}个元素", rule.getRuleName(), successCount, items.size());
|
||||
|
||||
// 6. 匹配第一条后停止
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("自动填充引擎执行异常, templateId={}, faceId={}", templateId, faceId, e);
|
||||
}
|
||||
|
||||
return dynamicData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.puzzle.fill.enums.ConditionType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 总是匹配策略(兜底规则)
|
||||
*/
|
||||
@Component
|
||||
public class AlwaysConditionStrategy implements ConditionStrategy {
|
||||
|
||||
@Override
|
||||
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return ConditionType.ALWAYS.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 条件评估上下文
|
||||
* 包含评估所需的各种运行时数据
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class ConditionContext {
|
||||
|
||||
/**
|
||||
* 人脸ID
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 机位数量(缓存值,避免重复查询)
|
||||
*/
|
||||
private Integer deviceCount;
|
||||
|
||||
/**
|
||||
* 机位ID列表(用于精确匹配指定的机位)
|
||||
*/
|
||||
private List<Long> deviceIds;
|
||||
|
||||
/**
|
||||
* 可扩展的其他上下文数据
|
||||
*/
|
||||
private Object extra;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 条件评估器
|
||||
* 使用策略模式,根据conditionType动态选择评估策略
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ConditionEvaluator {
|
||||
|
||||
private final Map<String, ConditionStrategy> strategyMap;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public ConditionEvaluator(List<ConditionStrategy> strategies, ObjectMapper objectMapper) {
|
||||
this.strategyMap = strategies.stream()
|
||||
.collect(Collectors.toMap(
|
||||
ConditionStrategy::getSupportedType,
|
||||
Function.identity()
|
||||
));
|
||||
this.objectMapper = objectMapper;
|
||||
log.info("初始化条件评估器,已注册{}个策略: {}", strategyMap.size(), strategyMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估规则条件是否匹配
|
||||
*
|
||||
* @param rule 规则实体
|
||||
* @param context 评估上下文
|
||||
* @return true-匹配, false-不匹配
|
||||
*/
|
||||
public boolean evaluate(PuzzleFillRuleEntity rule, ConditionContext context) {
|
||||
String conditionType = rule.getConditionType();
|
||||
ConditionStrategy strategy = strategyMap.get(conditionType);
|
||||
|
||||
if (strategy == null) {
|
||||
log.warn("未找到条件类型[{}]对应的评估策略,规则ID:{}", conditionType, rule.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode conditionValue = objectMapper.readTree(rule.getConditionValue());
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
log.debug("规则[{}]条件评估结果: {}, 条件类型: {}, 条件值: {}",
|
||||
rule.getRuleName(), result, conditionType, rule.getConditionValue());
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("规则[{}]条件评估异常,规则ID:{}", rule.getRuleName(), rule.getId(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* 条件评估策略接口
|
||||
* 使用策略模式,每种条件类型实现独立的评估逻辑,方便测试和扩展
|
||||
*/
|
||||
public interface ConditionStrategy {
|
||||
|
||||
/**
|
||||
* 评估条件是否匹配
|
||||
*
|
||||
* @param conditionValue 条件值(JSON)
|
||||
* @param context 评估上下文
|
||||
* @return true-匹配, false-不匹配
|
||||
*/
|
||||
boolean evaluate(JsonNode conditionValue, ConditionContext context);
|
||||
|
||||
/**
|
||||
* 获取支持的条件类型
|
||||
*/
|
||||
String getSupportedType();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.puzzle.fill.enums.ConditionType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 机位数量精确匹配策略
|
||||
*/
|
||||
@Component
|
||||
public class DeviceCountConditionStrategy implements ConditionStrategy {
|
||||
|
||||
@Override
|
||||
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
|
||||
if (conditionValue == null || !conditionValue.has("deviceCount")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int expectedCount = conditionValue.get("deviceCount").asInt();
|
||||
Integer actualCount = context.getDeviceCount();
|
||||
|
||||
if (actualCount == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return actualCount == expectedCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return ConditionType.DEVICE_COUNT.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.puzzle.fill.enums.ConditionType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 机位数量范围匹配策略
|
||||
*/
|
||||
@Component
|
||||
public class DeviceCountRangeConditionStrategy implements ConditionStrategy {
|
||||
|
||||
@Override
|
||||
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
|
||||
if (conditionValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer actualCount = context.getDeviceCount();
|
||||
if (actualCount == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer minCount = conditionValue.has("minCount") ? conditionValue.get("minCount").asInt() : null;
|
||||
Integer maxCount = conditionValue.has("maxCount") ? conditionValue.get("maxCount").asInt() : null;
|
||||
|
||||
if (minCount != null && actualCount < minCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxCount != null && actualCount > maxCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return ConditionType.DEVICE_COUNT_RANGE.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 机位ID匹配条件策略
|
||||
* 判断指定的机位ID是否在当前上下文的机位列表中
|
||||
*
|
||||
* 支持两种匹配模式:
|
||||
* 1. 单个机位匹配: {"deviceId": 123}
|
||||
* 2. 多个机位匹配: {"deviceIds": [123, 456], "matchMode": "ALL"/"ANY"}
|
||||
* - ALL: 所有指定的机位都必须存在
|
||||
* - ANY: 至少存在一个指定的机位(默认)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DeviceIdMatchConditionStrategy implements ConditionStrategy {
|
||||
|
||||
@Override
|
||||
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
|
||||
if (conditionValue == null) {
|
||||
log.warn("DEVICE_ID_MATCH条件值为null");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Long> contextDeviceIds = context.getDeviceIds();
|
||||
if (contextDeviceIds == null || contextDeviceIds.isEmpty()) {
|
||||
log.debug("上下文中没有机位ID列表");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 单个机位匹配模式
|
||||
if (conditionValue.has("deviceId")) {
|
||||
Long requiredDeviceId = conditionValue.get("deviceId").asLong();
|
||||
boolean matched = contextDeviceIds.contains(requiredDeviceId);
|
||||
log.debug("单机位匹配: deviceId={}, matched={}", requiredDeviceId, matched);
|
||||
return matched;
|
||||
}
|
||||
|
||||
// 多个机位匹配模式
|
||||
if (conditionValue.has("deviceIds")) {
|
||||
JsonNode deviceIdsNode = conditionValue.get("deviceIds");
|
||||
if (!deviceIdsNode.isArray()) {
|
||||
log.warn("deviceIds字段必须是数组");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Long> requiredDeviceIds = new ArrayList<>();
|
||||
deviceIdsNode.forEach(node -> requiredDeviceIds.add(node.asLong()));
|
||||
|
||||
if (requiredDeviceIds.isEmpty()) {
|
||||
log.warn("deviceIds数组为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取匹配模式,默认为ANY
|
||||
String matchMode = conditionValue.has("matchMode")
|
||||
? conditionValue.get("matchMode").asText()
|
||||
: "ANY";
|
||||
|
||||
boolean matched;
|
||||
if ("ALL".equalsIgnoreCase(matchMode)) {
|
||||
// ALL模式: 所有指定的机位都必须存在
|
||||
matched = contextDeviceIds.containsAll(requiredDeviceIds);
|
||||
log.debug("多机位ALL匹配: requiredDeviceIds={}, matched={}", requiredDeviceIds, matched);
|
||||
} else {
|
||||
// ANY模式: 至少存在一个指定的机位
|
||||
matched = requiredDeviceIds.stream().anyMatch(contextDeviceIds::contains);
|
||||
log.debug("多机位ANY匹配: requiredDeviceIds={}, matched={}", requiredDeviceIds, matched);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
log.warn("DEVICE_ID_MATCH条件缺少deviceId或deviceIds字段");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return "DEVICE_ID_MATCH";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 数据源解析上下文
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class DataSourceContext {
|
||||
|
||||
/**
|
||||
* 人脸ID
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 可扩展的其他上下文数据
|
||||
*/
|
||||
private Object extra;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据源解析器
|
||||
* 使用策略模式,根据dataSource类型动态选择解析策略
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DataSourceResolver {
|
||||
|
||||
private final Map<String, DataSourceStrategy> strategyMap;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public DataSourceResolver(List<DataSourceStrategy> strategies, ObjectMapper objectMapper) {
|
||||
this.strategyMap = strategies.stream()
|
||||
.collect(Collectors.toMap(
|
||||
DataSourceStrategy::getSupportedType,
|
||||
Function.identity()
|
||||
));
|
||||
this.objectMapper = objectMapper;
|
||||
log.info("初始化数据源解析器,已注册{}个策略: {}", strategyMap.size(), strategyMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据源,返回填充值
|
||||
*
|
||||
* @param dataSource 数据源类型
|
||||
* @param sourceFilterJson 过滤条件(JSON字符串)
|
||||
* @param sortStrategy 排序策略
|
||||
* @param fallbackValue 降级默认值
|
||||
* @param context 解析上下文
|
||||
* @return 填充值
|
||||
*/
|
||||
public String resolve(String dataSource,
|
||||
String sourceFilterJson,
|
||||
String sortStrategy,
|
||||
String fallbackValue,
|
||||
DataSourceContext context) {
|
||||
DataSourceStrategy strategy = strategyMap.get(dataSource);
|
||||
|
||||
if (strategy == null) {
|
||||
log.warn("未找到数据源类型[{}]对应的解析策略", dataSource);
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode sourceFilter = null;
|
||||
if (sourceFilterJson != null && !sourceFilterJson.isEmpty()) {
|
||||
sourceFilter = objectMapper.readTree(sourceFilterJson);
|
||||
}
|
||||
|
||||
String value = strategy.resolve(sourceFilter, sortStrategy, context);
|
||||
|
||||
if (value == null || value.isEmpty()) {
|
||||
log.debug("数据源[{}]解析结果为空,使用降级值: {}", dataSource, fallbackValue);
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch (Exception e) {
|
||||
log.error("数据源[{}]解析异常,使用降级值: {}", dataSource, fallbackValue, e);
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* 数据源解析策略接口
|
||||
* 使用策略模式,每种数据源类型实现独立的解析逻辑
|
||||
*/
|
||||
public interface DataSourceStrategy {
|
||||
|
||||
/**
|
||||
* 解析数据源,返回填充值
|
||||
*
|
||||
* @param sourceFilter 数据源过滤条件(JSON)
|
||||
* @param sortStrategy 排序策略
|
||||
* @param context 解析上下文
|
||||
* @return 填充值(通常是URL)
|
||||
*/
|
||||
String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context);
|
||||
|
||||
/**
|
||||
* 获取支持的数据源类型
|
||||
*/
|
||||
String getSupportedType();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.puzzle.fill.enums.DataSourceType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 设备图片数据源策略
|
||||
* 根据deviceIndex指定第N个设备的图片
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DeviceImageDataSourceStrategy implements DataSourceStrategy {
|
||||
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
@Override
|
||||
public String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context) {
|
||||
try {
|
||||
// 默认type=2(图片)
|
||||
Integer type = 2;
|
||||
if (sourceFilter != null && sourceFilter.has("type")) {
|
||||
type = sourceFilter.get("type").asInt();
|
||||
}
|
||||
|
||||
// 获取deviceIndex
|
||||
Integer deviceIndex = 0;
|
||||
if (sourceFilter != null && sourceFilter.has("deviceIndex")) {
|
||||
deviceIndex = sourceFilter.get("deviceIndex").asInt();
|
||||
}
|
||||
|
||||
// 使用默认策略
|
||||
if (sortStrategy == null || sortStrategy.isEmpty()) {
|
||||
sortStrategy = "LATEST";
|
||||
}
|
||||
|
||||
SourceEntity source = sourceMapper.getSourceByFaceAndDeviceIndex(
|
||||
context.getFaceId(),
|
||||
deviceIndex,
|
||||
type,
|
||||
sortStrategy
|
||||
);
|
||||
|
||||
if (source != null) {
|
||||
String url = type == 1 ? source.getVideoUrl() : source.getUrl();
|
||||
log.debug("解析DEVICE_IMAGE成功, faceId={}, deviceIndex={}, type={}, url={}",
|
||||
context.getFaceId(), deviceIndex, type, url);
|
||||
return url;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析DEVICE_IMAGE异常, faceId={}", context.getFaceId(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return DataSourceType.DEVICE_IMAGE.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.puzzle.fill.enums.DataSourceType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 人脸URL数据源策略
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FaceUrlDataSourceStrategy implements DataSourceStrategy {
|
||||
|
||||
@Autowired
|
||||
private FaceMapper faceMapper;
|
||||
|
||||
@Override
|
||||
public String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context) {
|
||||
try {
|
||||
FaceEntity face = faceMapper.get(context.getFaceId());
|
||||
if (face != null && face.getFaceUrl() != null) {
|
||||
log.debug("解析FACE_URL成功, faceId={}, url={}", context.getFaceId(), face.getFaceUrl());
|
||||
return face.getFaceUrl();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析FACE_URL异常, faceId={}", context.getFaceId(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return DataSourceType.FACE_URL.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ycwl.basic.puzzle.fill.enums.DataSourceType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 静态值数据源策略
|
||||
* 直接返回配置的静态值
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class StaticValueDataSourceStrategy implements DataSourceStrategy {
|
||||
|
||||
@Override
|
||||
public String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context) {
|
||||
if (sourceFilter != null && sourceFilter.has("value")) {
|
||||
String value = sourceFilter.get("value").asText();
|
||||
log.debug("解析STATIC_VALUE成功, value={}", value);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return DataSourceType.STATIC_VALUE.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ycwl.basic.puzzle.fill.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 填充规则条件类型枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum ConditionType {
|
||||
|
||||
/**
|
||||
* 机位数量匹配
|
||||
*/
|
||||
DEVICE_COUNT("DEVICE_COUNT", "机位数量匹配"),
|
||||
|
||||
/**
|
||||
* 机位数量范围
|
||||
*/
|
||||
DEVICE_COUNT_RANGE("DEVICE_COUNT_RANGE", "机位数量范围"),
|
||||
|
||||
/**
|
||||
* 机位ID精确匹配
|
||||
*/
|
||||
DEVICE_ID_MATCH("DEVICE_ID_MATCH", "机位ID精确匹配"),
|
||||
|
||||
/**
|
||||
* 总是匹配(兜底规则)
|
||||
*/
|
||||
ALWAYS("ALWAYS", "总是匹配");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
ConditionType(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举
|
||||
*/
|
||||
public static ConditionType fromCode(String code) {
|
||||
for (ConditionType type : values()) {
|
||||
if (type.code.equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知的条件类型: " + code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.ycwl.basic.puzzle.fill.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据源类型枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum DataSourceType {
|
||||
|
||||
/**
|
||||
* 人脸URL(来自face表的face_url)
|
||||
*/
|
||||
FACE_URL("FACE_URL", "人脸URL"),
|
||||
|
||||
/**
|
||||
* 素材图片(来自source表,type=2)
|
||||
*/
|
||||
SOURCE_IMAGE("SOURCE_IMAGE", "素材图片"),
|
||||
|
||||
/**
|
||||
* 素材视频(来自source表,type=1)
|
||||
*/
|
||||
SOURCE_VIDEO("SOURCE_VIDEO", "素材视频"),
|
||||
|
||||
/**
|
||||
* 设备图片(根据deviceIndex指定第N个设备的图片)
|
||||
*/
|
||||
DEVICE_IMAGE("DEVICE_IMAGE", "设备图片"),
|
||||
|
||||
/**
|
||||
* 静态值(直接使用fallbackValue)
|
||||
*/
|
||||
STATIC_VALUE("STATIC_VALUE", "静态值");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
DataSourceType(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举
|
||||
*/
|
||||
public static DataSourceType fromCode(String code) {
|
||||
for (DataSourceType type : values()) {
|
||||
if (type.code.equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知的数据源类型: " + code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ycwl.basic.puzzle.fill.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 素材排序策略枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum SortStrategy {
|
||||
|
||||
/**
|
||||
* 最新创建(按create_time DESC)
|
||||
*/
|
||||
LATEST("LATEST", "最新创建"),
|
||||
|
||||
/**
|
||||
* 最早创建(按create_time ASC)
|
||||
*/
|
||||
EARLIEST("EARLIEST", "最早创建"),
|
||||
|
||||
/**
|
||||
* 分数降序(按score DESC,需要source表有score字段)
|
||||
*/
|
||||
SCORE_DESC("SCORE_DESC", "分数降序"),
|
||||
|
||||
/**
|
||||
* 优先已购买(按is_buy DESC, create_time DESC)
|
||||
*/
|
||||
PURCHASED_FIRST("PURCHASED_FIRST", "优先已购买");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
SortStrategy(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举
|
||||
*/
|
||||
public static SortStrategy fromCode(String code) {
|
||||
for (SortStrategy strategy : values()) {
|
||||
if (strategy.code.equals(code)) {
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
return LATEST; // 默认返回最新
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ycwl.basic.puzzle.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图自动填充规则明细 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PuzzleFillRuleItemMapper extends BaseMapper<PuzzleFillRuleItemEntity> {
|
||||
|
||||
/**
|
||||
* 根据规则ID查询所有明细(按item_order升序)
|
||||
*
|
||||
* @param ruleId 规则ID
|
||||
* @return 明细列表
|
||||
*/
|
||||
List<PuzzleFillRuleItemEntity> listByRuleId(@Param("ruleId") Long ruleId);
|
||||
|
||||
/**
|
||||
* 批量插入明细
|
||||
*
|
||||
* @param items 明细列表
|
||||
* @return 插入数量
|
||||
*/
|
||||
int batchInsert(@Param("items") List<PuzzleFillRuleItemEntity> items);
|
||||
|
||||
/**
|
||||
* 根据规则ID删除所有明细
|
||||
*
|
||||
* @param ruleId 规则ID
|
||||
* @return 删除数量
|
||||
*/
|
||||
int deleteByRuleId(@Param("ruleId") Long ruleId);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ycwl.basic.puzzle.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图自动填充规则 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PuzzleFillRuleMapper extends BaseMapper<PuzzleFillRuleEntity> {
|
||||
|
||||
/**
|
||||
* 根据模板ID查询所有启用的规则(按优先级降序)
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
List<PuzzleFillRuleEntity> listByTemplateId(@Param("templateId") Long templateId);
|
||||
|
||||
/**
|
||||
* 根据模板ID和景区ID查询所有启用的规则(按优先级降序)
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
List<PuzzleFillRuleEntity> listByTemplateAndScenic(@Param("templateId") Long templateId, @Param("scenicId") Long scenicId);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ycwl.basic.puzzle.service;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleSaveRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图填充规则服务接口
|
||||
*/
|
||||
public interface IPuzzleFillRuleService {
|
||||
|
||||
/**
|
||||
* 创建规则(主+明细)
|
||||
*
|
||||
* @param request 保存请求
|
||||
* @return 规则ID
|
||||
*/
|
||||
Long create(PuzzleFillRuleSaveRequest request);
|
||||
|
||||
/**
|
||||
* 更新规则(主+明细)
|
||||
*
|
||||
* @param request 保存请求
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean update(PuzzleFillRuleSaveRequest request);
|
||||
|
||||
/**
|
||||
* 删除规则(级联删除明细)
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean delete(Long id);
|
||||
|
||||
/**
|
||||
* 查询单条规则(含明细)
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @return 规则DTO
|
||||
*/
|
||||
PuzzleFillRuleDTO getById(Long id);
|
||||
|
||||
/**
|
||||
* 查询模板的所有规则(含明细)
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
List<PuzzleFillRuleDTO> listByTemplateId(Long templateId);
|
||||
|
||||
/**
|
||||
* 启用/禁用规则
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @param enabled 是否启用
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean toggleEnabled(Long id, Integer enabled);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.ycwl.basic.puzzle.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleItemDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleSaveRequest;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleFillRuleService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 拼图填充规则服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PuzzleFillRuleServiceImpl implements IPuzzleFillRuleService {
|
||||
|
||||
@Autowired
|
||||
private PuzzleFillRuleMapper ruleMapper;
|
||||
|
||||
@Autowired
|
||||
private PuzzleFillRuleItemMapper itemMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(PuzzleFillRuleSaveRequest request) {
|
||||
// 1. 保存主规则
|
||||
PuzzleFillRuleEntity ruleEntity = new PuzzleFillRuleEntity();
|
||||
BeanUtils.copyProperties(request, ruleEntity);
|
||||
ruleMapper.insert(ruleEntity);
|
||||
|
||||
Long ruleId = ruleEntity.getId();
|
||||
log.info("创建填充规则成功, ruleId={}, ruleName={}", ruleId, request.getRuleName());
|
||||
|
||||
// 2. 批量保存明细
|
||||
if (request.getItems() != null && !request.getItems().isEmpty()) {
|
||||
List<PuzzleFillRuleItemEntity> itemEntities = request.getItems().stream()
|
||||
.map(dto -> {
|
||||
PuzzleFillRuleItemEntity entity = new PuzzleFillRuleItemEntity();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
entity.setRuleId(ruleId);
|
||||
return entity;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
itemMapper.batchInsert(itemEntities);
|
||||
log.info("批量保存规则明细成功, count={}", itemEntities.size());
|
||||
}
|
||||
|
||||
return ruleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean update(PuzzleFillRuleSaveRequest request) {
|
||||
if (request.getId() == null) {
|
||||
throw new IllegalArgumentException("更新规则时ID不能为空");
|
||||
}
|
||||
|
||||
// 1. 更新主规则
|
||||
PuzzleFillRuleEntity ruleEntity = new PuzzleFillRuleEntity();
|
||||
BeanUtils.copyProperties(request, ruleEntity);
|
||||
ruleMapper.updateById(ruleEntity);
|
||||
|
||||
// 2. 删除旧明细
|
||||
itemMapper.deleteByRuleId(request.getId());
|
||||
|
||||
// 3. 批量插入新明细
|
||||
if (request.getItems() != null && !request.getItems().isEmpty()) {
|
||||
List<PuzzleFillRuleItemEntity> itemEntities = request.getItems().stream()
|
||||
.map(dto -> {
|
||||
PuzzleFillRuleItemEntity entity = new PuzzleFillRuleItemEntity();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
entity.setRuleId(request.getId());
|
||||
return entity;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
itemMapper.batchInsert(itemEntities);
|
||||
}
|
||||
|
||||
log.info("更新填充规则成功, ruleId={}, itemCount={}", request.getId(),
|
||||
request.getItems() != null ? request.getItems().size() : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean delete(Long id) {
|
||||
// MyBatis-Plus会级联删除明细(通过外键ON DELETE CASCADE)
|
||||
ruleMapper.deleteById(id);
|
||||
log.info("删除填充规则成功, ruleId={}", id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PuzzleFillRuleDTO getById(Long id) {
|
||||
PuzzleFillRuleEntity ruleEntity = ruleMapper.selectById(id);
|
||||
if (ruleEntity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PuzzleFillRuleDTO dto = new PuzzleFillRuleDTO();
|
||||
BeanUtils.copyProperties(ruleEntity, dto);
|
||||
|
||||
// 查询明细
|
||||
List<PuzzleFillRuleItemEntity> itemEntities = itemMapper.listByRuleId(id);
|
||||
if (itemEntities != null && !itemEntities.isEmpty()) {
|
||||
List<PuzzleFillRuleItemDTO> itemDTOs = itemEntities.stream()
|
||||
.map(entity -> {
|
||||
PuzzleFillRuleItemDTO itemDTO = new PuzzleFillRuleItemDTO();
|
||||
BeanUtils.copyProperties(entity, itemDTO);
|
||||
return itemDTO;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
dto.setItems(itemDTOs);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PuzzleFillRuleDTO> listByTemplateId(Long templateId) {
|
||||
List<PuzzleFillRuleEntity> ruleEntities = ruleMapper.listByTemplateId(templateId);
|
||||
if (ruleEntities == null || ruleEntities.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return ruleEntities.stream()
|
||||
.map(ruleEntity -> {
|
||||
PuzzleFillRuleDTO dto = new PuzzleFillRuleDTO();
|
||||
BeanUtils.copyProperties(ruleEntity, dto);
|
||||
|
||||
// 查询明细
|
||||
List<PuzzleFillRuleItemEntity> itemEntities = itemMapper.listByRuleId(ruleEntity.getId());
|
||||
if (itemEntities != null && !itemEntities.isEmpty()) {
|
||||
List<PuzzleFillRuleItemDTO> itemDTOs = itemEntities.stream()
|
||||
.map(entity -> {
|
||||
PuzzleFillRuleItemDTO itemDTO = new PuzzleFillRuleItemDTO();
|
||||
BeanUtils.copyProperties(entity, itemDTO);
|
||||
return itemDTO;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
dto.setItems(itemDTOs);
|
||||
}
|
||||
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean toggleEnabled(Long id, Integer enabled) {
|
||||
LambdaUpdateWrapper<PuzzleFillRuleEntity> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(PuzzleFillRuleEntity::getId, id)
|
||||
.set(PuzzleFillRuleEntity::getEnabled, enabled);
|
||||
|
||||
int count = ruleMapper.update(null, updateWrapper);
|
||||
log.info("切换规则启用状态, ruleId={}, enabled={}", id, enabled);
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.fill.PuzzleElementFillEngine;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
@@ -23,7 +24,9 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -41,6 +44,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
private final PuzzleElementMapper elementMapper;
|
||||
private final PuzzleGenerationRecordMapper recordMapper;
|
||||
private final PuzzleImageRenderer imageRenderer;
|
||||
private final PuzzleElementFillEngine fillEngine;
|
||||
|
||||
@Override
|
||||
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
|
||||
@@ -67,13 +71,16 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||
|
||||
// 3. 创建生成记录
|
||||
// 3. 准备dynamicData(合并自动填充和手动数据)
|
||||
Map<String, String> finalDynamicData = buildDynamicData(template, request);
|
||||
|
||||
// 4. 创建生成记录
|
||||
PuzzleGenerationRecordEntity record = createRecord(template, request);
|
||||
recordMapper.insert(record);
|
||||
|
||||
try {
|
||||
// 4. 渲染图片
|
||||
BufferedImage resultImage = imageRenderer.render(template, elements, request.getDynamicData());
|
||||
// 5. 渲染图片
|
||||
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
|
||||
|
||||
// 5. 上传到OSS
|
||||
String imageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
|
||||
@@ -179,4 +186,39 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建dynamicData(合并自动填充和手动数据)
|
||||
* 优先级: 手动传入的数据 > 自动填充的数据
|
||||
*/
|
||||
private Map<String, String> buildDynamicData(PuzzleTemplateEntity template, PuzzleGenerateRequest request) {
|
||||
Map<String, String> dynamicData = new HashMap<>();
|
||||
|
||||
// 1. 自动填充(基于faceId和规则)
|
||||
if (request.getFaceId() != null && request.getScenicId() != null) {
|
||||
try {
|
||||
Map<String, String> autoFilled = fillEngine.execute(
|
||||
template.getId(),
|
||||
request.getFaceId(),
|
||||
request.getScenicId()
|
||||
);
|
||||
if (autoFilled != null && !autoFilled.isEmpty()) {
|
||||
dynamicData.putAll(autoFilled);
|
||||
log.info("自动填充成功, 填充了{}个元素", autoFilled.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("自动填充异常, templateId={}, faceId={}", template.getId(), request.getFaceId(), e);
|
||||
// 自动填充失败不影响整体流程,继续执行
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 手动数据覆盖(优先级更高)
|
||||
if (request.getDynamicData() != null && !request.getDynamicData().isEmpty()) {
|
||||
dynamicData.putAll(request.getDynamicData());
|
||||
log.debug("合并手动传入的dynamicData, count={}", request.getDynamicData().size());
|
||||
}
|
||||
|
||||
log.info("最终dynamicData: {}", dynamicData.keySet());
|
||||
return dynamicData;
|
||||
}
|
||||
}
|
||||
|
||||
43
src/main/resources/mapper/PuzzleFillRuleItemMapper.xml
Normal file
43
src/main/resources/mapper/PuzzleFillRuleItemMapper.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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.puzzle.mapper.PuzzleFillRuleItemMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="rule_id" property="ruleId"/>
|
||||
<result column="element_key" property="elementKey"/>
|
||||
<result column="data_source" property="dataSource"/>
|
||||
<result column="source_filter" property="sourceFilter"/>
|
||||
<result column="sort_strategy" property="sortStrategy"/>
|
||||
<result column="fallback_value" property="fallbackValue"/>
|
||||
<result column="item_order" property="itemOrder"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="listByRuleId" resultMap="BaseResultMap">
|
||||
SELECT *
|
||||
FROM puzzle_fill_rule_item
|
||||
WHERE rule_id = #{ruleId}
|
||||
ORDER BY item_order ASC, id ASC
|
||||
</select>
|
||||
|
||||
<insert id="batchInsert">
|
||||
INSERT INTO puzzle_fill_rule_item (
|
||||
rule_id, element_key, data_source, source_filter,
|
||||
sort_strategy, fallback_value, item_order
|
||||
) VALUES
|
||||
<foreach collection="items" item="item" separator=",">
|
||||
(
|
||||
#{item.ruleId}, #{item.elementKey}, #{item.dataSource}, #{item.sourceFilter},
|
||||
#{item.sortStrategy}, #{item.fallbackValue}, #{item.itemOrder}
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<delete id="deleteByRuleId">
|
||||
DELETE FROM puzzle_fill_rule_item
|
||||
WHERE rule_id = #{ruleId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
39
src/main/resources/mapper/PuzzleFillRuleMapper.xml
Normal file
39
src/main/resources/mapper/PuzzleFillRuleMapper.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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.puzzle.mapper.PuzzleFillRuleMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="template_id" property="templateId"/>
|
||||
<result column="rule_name" property="ruleName"/>
|
||||
<result column="condition_type" property="conditionType"/>
|
||||
<result column="condition_value" property="conditionValue"/>
|
||||
<result column="priority" property="priority"/>
|
||||
<result column="enabled" property="enabled"/>
|
||||
<result column="scenic_id" property="scenicId"/>
|
||||
<result column="description" property="description"/>
|
||||
<result column="deleted" property="deleted"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="listByTemplateId" resultMap="BaseResultMap">
|
||||
SELECT *
|
||||
FROM puzzle_fill_rule
|
||||
WHERE template_id = #{templateId}
|
||||
AND enabled = 1
|
||||
AND deleted = 0
|
||||
ORDER BY priority DESC, id ASC
|
||||
</select>
|
||||
|
||||
<select id="listByTemplateAndScenic" resultMap="BaseResultMap">
|
||||
SELECT *
|
||||
FROM puzzle_fill_rule
|
||||
WHERE template_id = #{templateId}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND enabled = 1
|
||||
AND deleted = 0
|
||||
ORDER BY priority DESC, id ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -361,4 +361,57 @@
|
||||
order by create_time desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countDistinctDevicesByFaceId" resultType="java.lang.Integer">
|
||||
SELECT COUNT(DISTINCT s.device_id)
|
||||
FROM member_source ms
|
||||
INNER JOIN source s ON ms.source_id = s.id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
AND s.type = 2
|
||||
AND s.deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="getSourceByFaceAndDeviceIndex" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
WITH ranked_sources AS (
|
||||
SELECT s.*,
|
||||
ROW_NUMBER() OVER (PARTITION BY s.device_id
|
||||
<choose>
|
||||
<when test='sortStrategy == "LATEST"'>
|
||||
ORDER BY s.create_time DESC
|
||||
</when>
|
||||
<when test='sortStrategy == "EARLIEST"'>
|
||||
ORDER BY s.create_time ASC
|
||||
</when>
|
||||
<when test='sortStrategy == "SCORE_DESC"'>
|
||||
ORDER BY IFNULL(s.score, 0) DESC, s.create_time DESC
|
||||
</when>
|
||||
<when test='sortStrategy == "PURCHASED_FIRST"'>
|
||||
ORDER BY ms.is_buy DESC, s.create_time DESC
|
||||
</when>
|
||||
<otherwise>
|
||||
ORDER BY s.create_time DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
) as rn,
|
||||
ROW_NUMBER() OVER (ORDER BY s.device_id ASC) as device_rank
|
||||
FROM source s
|
||||
INNER JOIN member_source ms ON s.id = ms.source_id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
AND s.type = #{type}
|
||||
AND s.deleted = 0
|
||||
)
|
||||
SELECT *
|
||||
FROM ranked_sources
|
||||
WHERE rn = 1 AND device_rank = #{deviceIndex} + 1
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="getDeviceIdsByFaceId" resultType="java.lang.Long">
|
||||
SELECT DISTINCT s.device_id
|
||||
FROM member_source ms
|
||||
INNER JOIN source s ON ms.source_id = s.id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
AND s.deleted = 0
|
||||
ORDER BY s.device_id ASC
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.ycwl.basic.puzzle.fill;
|
||||
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
|
||||
import com.ycwl.basic.puzzle.fill.condition.ConditionEvaluator;
|
||||
import com.ycwl.basic.puzzle.fill.datasource.DataSourceResolver;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 拼图元素填充引擎测试
|
||||
*/
|
||||
@DisplayName("拼图元素填充引擎测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PuzzleElementFillEngineTest {
|
||||
|
||||
@Mock
|
||||
private PuzzleFillRuleMapper ruleMapper;
|
||||
|
||||
@Mock
|
||||
private PuzzleFillRuleItemMapper itemMapper;
|
||||
|
||||
@Mock
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
@Mock
|
||||
private ConditionEvaluator conditionEvaluator;
|
||||
|
||||
@Mock
|
||||
private DataSourceResolver dataSourceResolver;
|
||||
|
||||
@InjectMocks
|
||||
private PuzzleElementFillEngine engine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 默认设置
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当没有配置规则时应该返回空Map")
|
||||
void shouldReturnEmptyMapWhenNoRulesConfigured() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(new ArrayList<>());
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty());
|
||||
verify(ruleMapper, times(1)).listByTemplateAndScenic(templateId, scenicId);
|
||||
verify(sourceMapper, never()).countDistinctDevicesByFaceId(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功执行匹配的规则并填充多个元素")
|
||||
void shouldExecuteMatchedRuleAndFillMultipleElements() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
// 模拟规则
|
||||
PuzzleFillRuleEntity rule = createRule(1L, "4机位规则", 100);
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule));
|
||||
|
||||
// 模拟机位数量和机位列表
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
|
||||
// 模拟条件匹配
|
||||
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
||||
|
||||
// 模拟规则明细
|
||||
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
||||
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":0}", "LATEST"),
|
||||
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
|
||||
createItem(3L, 1L, "sfp_3", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST"),
|
||||
createItem(4L, 1L, "sfp_4", "DEVICE_IMAGE", "{\"deviceIndex\":3}", "LATEST")
|
||||
);
|
||||
when(itemMapper.listByRuleId(1L)).thenReturn(items);
|
||||
|
||||
// 模拟数据源解析
|
||||
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn("https://oss.example.com/img1.jpg")
|
||||
.thenReturn("https://oss.example.com/img2.jpg")
|
||||
.thenReturn("https://oss.example.com/img3.jpg")
|
||||
.thenReturn("https://oss.example.com/img4.jpg");
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertEquals(4, result.size());
|
||||
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
|
||||
assertEquals("https://oss.example.com/img2.jpg", result.get("sfp_2"));
|
||||
assertEquals("https://oss.example.com/img3.jpg", result.get("sfp_3"));
|
||||
assertEquals("https://oss.example.com/img4.jpg", result.get("sfp_4"));
|
||||
|
||||
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
|
||||
verify(itemMapper, times(1)).listByRuleId(1L);
|
||||
verify(dataSourceResolver, times(4)).resolve(anyString(), anyString(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该按优先级顺序评估规则并在匹配第一条后停止")
|
||||
void shouldEvaluateRulesByPriorityAndStopAfterFirstMatch() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
// 模拟3条规则(按priority DESC排序)
|
||||
PuzzleFillRuleEntity rule1 = createRule(1L, "高优先级规则", 100);
|
||||
PuzzleFillRuleEntity rule2 = createRule(2L, "中优先级规则", 50);
|
||||
PuzzleFillRuleEntity rule3 = createRule(3L, "低优先级规则", 10);
|
||||
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule1, rule2, rule3));
|
||||
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
|
||||
// 模拟第一条规则不匹配,第二条匹配
|
||||
when(conditionEvaluator.evaluate(eq(rule1), any())).thenReturn(false);
|
||||
when(conditionEvaluator.evaluate(eq(rule2), any())).thenReturn(true);
|
||||
|
||||
// 模拟规则2的明细
|
||||
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
||||
createItem(1L, 2L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST")
|
||||
);
|
||||
when(itemMapper.listByRuleId(2L)).thenReturn(items);
|
||||
|
||||
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn("https://oss.example.com/img.jpg");
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("https://oss.example.com/img.jpg", result.get("sfp_1"));
|
||||
|
||||
// 应该评估了rule1和rule2,但没有评估rule3(因为rule2匹配后停止)
|
||||
verify(conditionEvaluator, times(1)).evaluate(eq(rule1), any());
|
||||
verify(conditionEvaluator, times(1)).evaluate(eq(rule2), any());
|
||||
verify(conditionEvaluator, never()).evaluate(eq(rule3), any());
|
||||
|
||||
verify(itemMapper, times(1)).listByRuleId(2L);
|
||||
verify(itemMapper, never()).listByRuleId(1L);
|
||||
verify(itemMapper, never()).listByRuleId(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当所有规则都不匹配时应该返回空Map")
|
||||
void shouldReturnEmptyMapWhenNoRuleMatches() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
PuzzleFillRuleEntity rule1 = createRule(1L, "规则1", 100);
|
||||
PuzzleFillRuleEntity rule2 = createRule(2L, "规则2", 50);
|
||||
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule1, rule2));
|
||||
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
|
||||
// 所有规则都不匹配
|
||||
when(conditionEvaluator.evaluate(any(), any())).thenReturn(false);
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty());
|
||||
verify(conditionEvaluator, times(2)).evaluate(any(), any());
|
||||
verify(itemMapper, never()).listByRuleId(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当数据源解析返回null时应该跳过该元素")
|
||||
void shouldSkipElementWhenDataSourceReturnsNull() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule));
|
||||
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
||||
|
||||
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
||||
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST"),
|
||||
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{}", "LATEST")
|
||||
);
|
||||
when(itemMapper.listByRuleId(1L)).thenReturn(items);
|
||||
|
||||
// 第一个返回值,第二个返回null
|
||||
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn("https://oss.example.com/img1.jpg")
|
||||
.thenReturn(null);
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
|
||||
assertFalse(result.containsKey("sfp_2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当数据源解析失败时应该使用fallbackValue")
|
||||
void shouldUseFallbackValueWhenDataSourceFails() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule));
|
||||
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
||||
|
||||
// 明细包含fallbackValue
|
||||
PuzzleFillRuleItemEntity item = createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST");
|
||||
item.setFallbackValue("https://oss.example.com/default.jpg");
|
||||
|
||||
when(itemMapper.listByRuleId(1L)).thenReturn(Arrays.asList(item));
|
||||
|
||||
// DataSourceResolver内部会处理fallback,这里模拟返回fallback值
|
||||
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), eq("https://oss.example.com/default.jpg"), any()))
|
||||
.thenReturn("https://oss.example.com/default.jpg");
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("https://oss.example.com/default.jpg", result.get("sfp_1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当规则匹配但没有明细时应该返回空Map")
|
||||
void shouldReturnEmptyMapWhenRuleMatchesButHasNoItems() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
PuzzleFillRuleEntity rule = createRule(1L, "空明细规则", 100);
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule));
|
||||
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
||||
|
||||
// 规则没有明细
|
||||
when(itemMapper.listByRuleId(1L)).thenReturn(new ArrayList<>());
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当发生异常时应该返回空Map并记录日志")
|
||||
void shouldReturnEmptyMapAndLogWhenExceptionOccurs() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenThrow(new RuntimeException("Database error"));
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持DEVICE_ID_MATCH条件类型并正确填充")
|
||||
void shouldSupportDeviceIdMatchCondition() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
Long faceId = 123L;
|
||||
Long scenicId = 1L;
|
||||
|
||||
// 模拟规则 - 使用DEVICE_ID_MATCH条件
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(1L);
|
||||
rule.setRuleName("指定机位规则");
|
||||
rule.setPriority(100);
|
||||
rule.setConditionType("DEVICE_ID_MATCH");
|
||||
rule.setConditionValue("{\"deviceIds\": [200, 300], \"matchMode\": \"ALL\"}");
|
||||
rule.setEnabled(1);
|
||||
|
||||
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
|
||||
.thenReturn(Arrays.asList(rule));
|
||||
|
||||
// 模拟机位数量和机位列表
|
||||
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
||||
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
||||
|
||||
// 模拟条件匹配
|
||||
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
||||
|
||||
// 模拟规则明细
|
||||
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
||||
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
|
||||
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST")
|
||||
);
|
||||
when(itemMapper.listByRuleId(1L)).thenReturn(items);
|
||||
|
||||
// 模拟数据源解析
|
||||
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn("https://oss.example.com/device200.jpg")
|
||||
.thenReturn("https://oss.example.com/device300.jpg");
|
||||
|
||||
// When
|
||||
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("https://oss.example.com/device200.jpg", result.get("sfp_1"));
|
||||
assertEquals("https://oss.example.com/device300.jpg", result.get("sfp_2"));
|
||||
|
||||
// 验证deviceIds被传递到ConditionContext
|
||||
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
|
||||
verify(sourceMapper, times(1)).getDeviceIdsByFaceId(faceId);
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private PuzzleFillRuleEntity createRule(Long id, String name, Integer priority) {
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(id);
|
||||
rule.setRuleName(name);
|
||||
rule.setPriority(priority);
|
||||
rule.setConditionType("DEVICE_COUNT");
|
||||
rule.setConditionValue("{\"deviceCount\": 4}");
|
||||
rule.setEnabled(1);
|
||||
return rule;
|
||||
}
|
||||
|
||||
private PuzzleFillRuleItemEntity createItem(Long id, Long ruleId, String elementKey,
|
||||
String dataSource, String sourceFilter, String sortStrategy) {
|
||||
PuzzleFillRuleItemEntity item = new PuzzleFillRuleItemEntity();
|
||||
item.setId(id);
|
||||
item.setRuleId(ruleId);
|
||||
item.setElementKey(elementKey);
|
||||
item.setDataSource(dataSource);
|
||||
item.setSourceFilter(sourceFilter);
|
||||
item.setSortStrategy(sortStrategy);
|
||||
item.setItemOrder(id.intValue());
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 总是匹配策略测试
|
||||
*/
|
||||
@DisplayName("总是匹配策略测试")
|
||||
class AlwaysConditionStrategyTest {
|
||||
|
||||
private AlwaysConditionStrategy strategy;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
strategy = new AlwaysConditionStrategy();
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("ALWAYS", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无论conditionValue和context如何都应该返回true")
|
||||
void shouldAlwaysReturnTrue() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"anyField\": \"anyValue\"}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.faceId(123L)
|
||||
.scenicId(1L)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当conditionValue为null时也应该返回true")
|
||||
void shouldReturnTrueEvenWhenConditionValueIsNull() {
|
||||
// Given
|
||||
ConditionContext context = ConditionContext.builder().build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(null, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当context为空时也应该返回true")
|
||||
void shouldReturnTrueEvenWhenContextIsEmpty() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder().build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 条件评估器集成测试
|
||||
*/
|
||||
@DisplayName("条件评估器集成测试")
|
||||
class ConditionEvaluatorTest {
|
||||
|
||||
private ConditionEvaluator evaluator;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
// 注册所有策略
|
||||
List<ConditionStrategy> strategies = Arrays.asList(
|
||||
new DeviceCountConditionStrategy(),
|
||||
new DeviceCountRangeConditionStrategy(),
|
||||
new DeviceIdMatchConditionStrategy(),
|
||||
new AlwaysConditionStrategy()
|
||||
);
|
||||
|
||||
evaluator = new ConditionEvaluator(strategies, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确评估DEVICE_COUNT类型的规则")
|
||||
void shouldEvaluateDeviceCountRule() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(1L);
|
||||
rule.setRuleName("4机位规则");
|
||||
rule.setConditionType("DEVICE_COUNT");
|
||||
rule.setConditionValue("{\"deviceCount\": 4}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确评估DEVICE_COUNT_RANGE类型的规则")
|
||||
void shouldEvaluateDeviceCountRangeRule() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(2L);
|
||||
rule.setRuleName("2-5机位规则");
|
||||
rule.setConditionType("DEVICE_COUNT_RANGE");
|
||||
rule.setConditionValue("{\"minCount\": 2, \"maxCount\": 5}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(3)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确评估ALWAYS类型的规则")
|
||||
void shouldEvaluateAlwaysRule() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(3L);
|
||||
rule.setRuleName("兜底规则");
|
||||
rule.setConditionType("ALWAYS");
|
||||
rule.setConditionValue("{}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(999)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确评估DEVICE_ID_MATCH类型的规则-单机位匹配")
|
||||
void shouldEvaluateDeviceIdMatchRuleSingleDevice() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(4L);
|
||||
rule.setRuleName("指定机位规则");
|
||||
rule.setConditionType("DEVICE_ID_MATCH");
|
||||
rule.setConditionValue("{\"deviceId\": 200}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(Arrays.asList(100L, 200L, 300L))
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确评估DEVICE_ID_MATCH类型的规则-多机位ANY匹配")
|
||||
void shouldEvaluateDeviceIdMatchRuleMultipleDevicesAny() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(5L);
|
||||
rule.setRuleName("多机位ANY规则");
|
||||
rule.setConditionType("DEVICE_ID_MATCH");
|
||||
rule.setConditionValue("{\"deviceIds\": [200, 400], \"matchMode\": \"ANY\"}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(Arrays.asList(100L, 200L, 300L))
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确评估DEVICE_ID_MATCH类型的规则-多机位ALL匹配")
|
||||
void shouldEvaluateDeviceIdMatchRuleMultipleDevicesAll() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(6L);
|
||||
rule.setRuleName("多机位ALL规则");
|
||||
rule.setConditionType("DEVICE_ID_MATCH");
|
||||
rule.setConditionValue("{\"deviceIds\": [100, 200], \"matchMode\": \"ALL\"}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(Arrays.asList(100L, 200L, 300L))
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当条件类型不存在时应该返回false")
|
||||
void shouldReturnFalseWhenConditionTypeNotFound() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(4L);
|
||||
rule.setRuleName("未知类型规则");
|
||||
rule.setConditionType("UNKNOWN_TYPE");
|
||||
rule.setConditionValue("{}");
|
||||
|
||||
ConditionContext context = ConditionContext.builder().build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当conditionValue格式错误时应该返回false")
|
||||
void shouldReturnFalseWhenConditionValueIsInvalid() {
|
||||
// Given
|
||||
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
||||
rule.setId(5L);
|
||||
rule.setRuleName("格式错误规则");
|
||||
rule.setConditionType("DEVICE_COUNT");
|
||||
rule.setConditionValue("invalid json");
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = evaluator.evaluate(rule, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持多个策略并正确路由")
|
||||
void shouldSupportMultipleStrategies() {
|
||||
// Test DEVICE_COUNT
|
||||
ConditionContext context1 = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.build();
|
||||
|
||||
PuzzleFillRuleEntity rule1 = new PuzzleFillRuleEntity();
|
||||
rule1.setConditionType("DEVICE_COUNT");
|
||||
rule1.setConditionValue("{\"deviceCount\": 4}");
|
||||
assertTrue(evaluator.evaluate(rule1, context1));
|
||||
|
||||
// Test DEVICE_COUNT_RANGE
|
||||
PuzzleFillRuleEntity rule2 = new PuzzleFillRuleEntity();
|
||||
rule2.setConditionType("DEVICE_COUNT_RANGE");
|
||||
rule2.setConditionValue("{\"minCount\": 2, \"maxCount\": 5}");
|
||||
assertTrue(evaluator.evaluate(rule2, context1));
|
||||
|
||||
// Test DEVICE_ID_MATCH
|
||||
ConditionContext context2 = ConditionContext.builder()
|
||||
.deviceIds(Arrays.asList(100L, 200L, 300L))
|
||||
.build();
|
||||
|
||||
PuzzleFillRuleEntity rule3 = new PuzzleFillRuleEntity();
|
||||
rule3.setConditionType("DEVICE_ID_MATCH");
|
||||
rule3.setConditionValue("{\"deviceId\": 200}");
|
||||
assertTrue(evaluator.evaluate(rule3, context2));
|
||||
|
||||
// Test ALWAYS
|
||||
PuzzleFillRuleEntity rule4 = new PuzzleFillRuleEntity();
|
||||
rule4.setConditionType("ALWAYS");
|
||||
rule4.setConditionValue("{}");
|
||||
assertTrue(evaluator.evaluate(rule4, context1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 机位数量精确匹配策略测试
|
||||
*/
|
||||
@DisplayName("机位数量精确匹配策略测试")
|
||||
class DeviceCountConditionStrategyTest {
|
||||
|
||||
private DeviceCountConditionStrategy strategy;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
strategy = new DeviceCountConditionStrategy();
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("DEVICE_COUNT", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量完全匹配时应该返回true")
|
||||
void shouldReturnTrueWhenDeviceCountMatches() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"deviceCount\": 4}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量不匹配时应该返回false")
|
||||
void shouldReturnFalseWhenDeviceCountDoesNotMatch() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"deviceCount\": 4}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(2)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当conditionValue为null时应该返回false")
|
||||
void shouldReturnFalseWhenConditionValueIsNull() {
|
||||
// Given
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(null, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当conditionValue缺少deviceCount字段时应该返回false")
|
||||
void shouldReturnFalseWhenDeviceCountFieldIsMissing() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"otherField\": 4}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(4)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当context中deviceCount为null时应该返回false")
|
||||
void shouldReturnFalseWhenContextDeviceCountIsNull() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"deviceCount\": 4}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(null)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持deviceCount为0的情况")
|
||||
void shouldSupportZeroDeviceCount() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"deviceCount\": 0}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(0)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 机位数量范围匹配策略测试
|
||||
*/
|
||||
@DisplayName("机位数量范围匹配策略测试")
|
||||
class DeviceCountRangeConditionStrategyTest {
|
||||
|
||||
private DeviceCountRangeConditionStrategy strategy;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
strategy = new DeviceCountRangeConditionStrategy();
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("DEVICE_COUNT_RANGE", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量在范围内时应该返回true")
|
||||
void shouldReturnTrueWhenDeviceCountInRange() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(3)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量等于最小值时应该返回true")
|
||||
void shouldReturnTrueWhenDeviceCountEqualsMin() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(2)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量等于最大值时应该返回true")
|
||||
void shouldReturnTrueWhenDeviceCountEqualsMax() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(5)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量小于最小值时应该返回false")
|
||||
void shouldReturnFalseWhenDeviceCountBelowMin() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(1)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当机位数量大于最大值时应该返回false")
|
||||
void shouldReturnFalseWhenDeviceCountAboveMax() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(6)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("仅指定最小值时应该正确验证")
|
||||
void shouldValidateWithMinCountOnly() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(3)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("仅指定最大值时应该正确验证")
|
||||
void shouldValidateWithMaxCountOnly() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(3)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当conditionValue为null时应该返回false")
|
||||
void shouldReturnFalseWhenConditionValueIsNull() {
|
||||
// Given
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(3)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(null, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当context中deviceCount为null时应该返回false")
|
||||
void shouldReturnFalseWhenContextDeviceCountIsNull() throws Exception {
|
||||
// Given
|
||||
String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}";
|
||||
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
|
||||
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(null)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package com.ycwl.basic.puzzle.fill.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 机位ID匹配条件策略测试
|
||||
*/
|
||||
@DisplayName("机位ID匹配条件策略测试")
|
||||
class DeviceIdMatchConditionStrategyTest {
|
||||
|
||||
private DeviceIdMatchConditionStrategy strategy;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
strategy = new DeviceIdMatchConditionStrategy();
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("DEVICE_ID_MATCH", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("单机位匹配-机位存在应该返回true")
|
||||
void shouldReturnTrueWhenSingleDeviceIdExists() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 200}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("单机位匹配-机位不存在应该返回false")
|
||||
void shouldReturnFalseWhenSingleDeviceIdNotExists() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 999}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位ANY匹配-至少存在一个机位应该返回true")
|
||||
void shouldReturnTrueWhenAnyDeviceIdMatches() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
// 200存在,400不存在,但ANY模式只需一个匹配
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200, 400], \"matchMode\": \"ANY\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位ANY匹配-没有任何机位存在应该返回false")
|
||||
void shouldReturnFalseWhenNoDeviceIdMatches() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [400, 500], \"matchMode\": \"ANY\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位ALL匹配-所有机位都存在应该返回true")
|
||||
void shouldReturnTrueWhenAllDeviceIdsMatch() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [100, 200], \"matchMode\": \"ALL\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位ALL匹配-部分机位不存在应该返回false")
|
||||
void shouldReturnFalseWhenNotAllDeviceIdsMatch() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
// 200存在但400不存在
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200, 400], \"matchMode\": \"ALL\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位匹配-未指定matchMode应该默认使用ANY模式")
|
||||
void shouldDefaultToAnyModeWhenMatchModeNotSpecified() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
// 未指定matchMode,默认ANY,200存在即可
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200, 400]}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位匹配-matchMode不区分大小写")
|
||||
void shouldBeCaseInsensitiveForMatchMode() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
// 使用小写all
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [100, 200], \"matchMode\": \"all\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当conditionValue为null时应该返回false")
|
||||
void shouldReturnFalseWhenConditionValueIsNull() {
|
||||
// Given
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(Arrays.asList(100L, 200L))
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(null, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当上下文中机位列表为null时应该返回false")
|
||||
void shouldReturnFalseWhenContextDeviceIdsIsNull() throws Exception {
|
||||
// Given
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(null)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 100}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当上下文中机位列表为空时应该返回false")
|
||||
void shouldReturnFalseWhenContextDeviceIdsIsEmpty() throws Exception {
|
||||
// Given
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(Collections.emptyList())
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 100}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当deviceIds不是数组时应该返回false")
|
||||
void shouldReturnFalseWhenDeviceIdsIsNotArray() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
// deviceIds不是数组而是字符串
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": \"100,200\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当deviceIds数组为空时应该返回false")
|
||||
void shouldReturnFalseWhenDeviceIdsArrayIsEmpty() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": []}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当缺少deviceId和deviceIds字段时应该返回false")
|
||||
void shouldReturnFalseWhenBothFieldsAreMissing() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"otherField\": \"test\"}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持单个机位匹配边界情况-机位ID为0")
|
||||
void shouldSupportZeroDeviceId() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(0L, 100L, 200L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 0}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持大数值机位ID")
|
||||
void shouldSupportLargeDeviceId() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(999999999999L, 100L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 999999999999}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多机位匹配-单个元素的数组应该正常工作")
|
||||
void shouldWorkWithSingleElementArray() throws Exception {
|
||||
// Given
|
||||
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceIds(deviceIds)
|
||||
.build();
|
||||
|
||||
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200]}");
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 设备图片数据源策略测试
|
||||
*/
|
||||
@DisplayName("设备图片数据源策略测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DeviceImageDataSourceStrategyTest {
|
||||
|
||||
@Mock
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
@InjectMocks
|
||||
private DeviceImageDataSourceStrategy strategy;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("DEVICE_IMAGE", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回指定设备的图片URL")
|
||||
void shouldReturnDeviceImageUrl() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
Integer deviceIndex = 0;
|
||||
String expectedUrl = "https://oss.example.com/device/img1.jpg";
|
||||
|
||||
SourceEntity source = new SourceEntity();
|
||||
source.setId(1L);
|
||||
source.setType(2); // 图片类型
|
||||
source.setUrl(expectedUrl);
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 2, "LATEST"))
|
||||
.thenReturn(source);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2, \"deviceIndex\": 0}");
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
assertEquals(expectedUrl, result);
|
||||
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 2, "LATEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当type为1(视频)时应该返回videoUrl")
|
||||
void shouldReturnVideoUrlWhenTypeIs1() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
Integer deviceIndex = 1;
|
||||
String expectedUrl = "https://oss.example.com/device/video1.mp4";
|
||||
|
||||
SourceEntity source = new SourceEntity();
|
||||
source.setId(2L);
|
||||
source.setType(1); // 视频类型
|
||||
source.setVideoUrl(expectedUrl);
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 1, "LATEST"))
|
||||
.thenReturn(source);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 1, \"deviceIndex\": 1}");
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
assertEquals(expectedUrl, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当未指定type时应该默认使用type=2(图片)")
|
||||
void shouldDefaultToImageTypeWhenTypeNotSpecified() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
Integer deviceIndex = 0;
|
||||
|
||||
SourceEntity source = new SourceEntity();
|
||||
source.setType(2);
|
||||
source.setUrl("https://oss.example.com/default.jpg");
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 2, "LATEST"))
|
||||
.thenReturn(source);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"deviceIndex\": 0}");
|
||||
|
||||
// When
|
||||
strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(eq(faceId), eq(deviceIndex), eq(2), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当未指定deviceIndex时应该默认使用0")
|
||||
void shouldDefaultToZeroWhenDeviceIndexNotSpecified() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
|
||||
SourceEntity source = new SourceEntity();
|
||||
source.setType(2);
|
||||
source.setUrl("https://oss.example.com/default.jpg");
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, 0, 2, "LATEST"))
|
||||
.thenReturn(source);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2}");
|
||||
|
||||
// When
|
||||
strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(eq(faceId), eq(0), anyInt(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当source不存在时应该返回null")
|
||||
void shouldReturnNullWhenSourceNotFound() throws Exception {
|
||||
// Given
|
||||
Long faceId = 999L;
|
||||
Integer deviceIndex = 0;
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), anyString()))
|
||||
.thenReturn(null);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2, \"deviceIndex\": 0}");
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当未指定sortStrategy时应该默认使用LATEST")
|
||||
void shouldDefaultToLatestSortStrategy() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
|
||||
SourceEntity source = new SourceEntity();
|
||||
source.setType(2);
|
||||
source.setUrl("https://oss.example.com/img.jpg");
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), eq("LATEST")))
|
||||
.thenReturn(source);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"deviceIndex\": 0}");
|
||||
|
||||
// When
|
||||
strategy.resolve(sourceFilter, null, context);
|
||||
|
||||
// Then
|
||||
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), eq("LATEST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当sourceMapper抛出异常时应该返回null")
|
||||
void shouldReturnNullWhenSourceMapperThrowsException() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
|
||||
when(sourceMapper.getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), anyString()))
|
||||
.thenThrow(new RuntimeException("Database error"));
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"deviceIndex\": 0}");
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 人脸URL数据源策略测试
|
||||
*/
|
||||
@DisplayName("人脸URL数据源策略测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FaceUrlDataSourceStrategyTest {
|
||||
|
||||
@Mock
|
||||
private FaceMapper faceMapper;
|
||||
|
||||
@InjectMocks
|
||||
private FaceUrlDataSourceStrategy strategy;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("FACE_URL", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当face存在且有faceUrl时应该返回URL")
|
||||
void shouldReturnFaceUrlWhenFaceExists() {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
String expectedUrl = "https://oss.example.com/face/123.jpg";
|
||||
|
||||
FaceEntity face = new FaceEntity();
|
||||
face.setId(faceId);
|
||||
face.setFaceUrl(expectedUrl);
|
||||
|
||||
when(faceMapper.get(faceId)).thenReturn(face);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(null, null, context);
|
||||
|
||||
// Then
|
||||
assertEquals(expectedUrl, result);
|
||||
verify(faceMapper, times(1)).get(faceId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当face不存在时应该返回null")
|
||||
void shouldReturnNullWhenFaceNotFound() {
|
||||
// Given
|
||||
Long faceId = 999L;
|
||||
|
||||
when(faceMapper.get(faceId)).thenReturn(null);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(null, null, context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
verify(faceMapper, times(1)).get(faceId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当face存在但faceUrl为null时应该返回null")
|
||||
void shouldReturnNullWhenFaceUrlIsNull() {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
|
||||
FaceEntity face = new FaceEntity();
|
||||
face.setId(faceId);
|
||||
face.setFaceUrl(null);
|
||||
|
||||
when(faceMapper.get(faceId)).thenReturn(face);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(null, null, context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当faceMapper抛出异常时应该返回null")
|
||||
void shouldReturnNullWhenFaceMapperThrowsException() {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
|
||||
when(faceMapper.get(anyLong())).thenThrow(new RuntimeException("Database error"));
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(null, null, context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sourceFilter和sortStrategy参数应该被忽略")
|
||||
void shouldIgnoreSourceFilterAndSortStrategy() throws Exception {
|
||||
// Given
|
||||
Long faceId = 123L;
|
||||
String expectedUrl = "https://oss.example.com/face/123.jpg";
|
||||
|
||||
FaceEntity face = new FaceEntity();
|
||||
face.setId(faceId);
|
||||
face.setFaceUrl(expectedUrl);
|
||||
|
||||
when(faceMapper.get(faceId)).thenReturn(face);
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder()
|
||||
.faceId(faceId)
|
||||
.build();
|
||||
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2}");
|
||||
String sortStrategy = "LATEST";
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, sortStrategy, context);
|
||||
|
||||
// Then
|
||||
assertEquals(expectedUrl, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.ycwl.basic.puzzle.fill.datasource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 静态值数据源策略测试
|
||||
*/
|
||||
@DisplayName("静态值数据源策略测试")
|
||||
class StaticValueDataSourceStrategyTest {
|
||||
|
||||
private StaticValueDataSourceStrategy strategy;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
strategy = new StaticValueDataSourceStrategy();
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回正确的支持类型")
|
||||
void shouldReturnCorrectSupportedType() {
|
||||
assertEquals("STATIC_VALUE", strategy.getSupportedType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该返回sourceFilter中的value值")
|
||||
void shouldReturnValueFromSourceFilter() throws Exception {
|
||||
// Given
|
||||
String expectedValue = "https://oss.example.com/static/default.jpg";
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"value\": \"" + expectedValue + "\"}");
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder().build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, null, context);
|
||||
|
||||
// Then
|
||||
assertEquals(expectedValue, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当sourceFilter为null时应该返回null")
|
||||
void shouldReturnNullWhenSourceFilterIsNull() {
|
||||
// Given
|
||||
DataSourceContext context = DataSourceContext.builder().build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(null, null, context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当sourceFilter缺少value字段时应该返回null")
|
||||
void shouldReturnNullWhenValueFieldIsMissing() throws Exception {
|
||||
// Given
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"otherField\": \"test\"}");
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder().build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, null, context);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sortStrategy参数应该被忽略")
|
||||
void shouldIgnoreSortStrategy() throws Exception {
|
||||
// Given
|
||||
String expectedValue = "static_value";
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"value\": \"" + expectedValue + "\"}");
|
||||
|
||||
DataSourceContext context = DataSourceContext.builder().build();
|
||||
|
||||
// When
|
||||
String result = strategy.resolve(sourceFilter, "LATEST", context);
|
||||
|
||||
// Then
|
||||
assertEquals(expectedValue, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持各种类型的字符串值")
|
||||
void shouldSupportVariousStringValues() throws Exception {
|
||||
// 测试URL
|
||||
testStaticValue("https://example.com/image.jpg");
|
||||
|
||||
// 测试普通文本
|
||||
testStaticValue("Hello World");
|
||||
|
||||
// 测试空字符串
|
||||
testStaticValue("");
|
||||
|
||||
// 测试包含特殊字符的文本
|
||||
testStaticValue("测试中文/Special!@#$%");
|
||||
}
|
||||
|
||||
private void testStaticValue(String value) throws Exception {
|
||||
JsonNode sourceFilter = objectMapper.readTree("{\"value\": \"" + value + "\"}");
|
||||
DataSourceContext context = DataSourceContext.builder().build();
|
||||
String result = strategy.resolve(sourceFilter, null, context);
|
||||
assertEquals(value, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.ycwl.basic.puzzle.service.impl;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleItemDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleSaveRequest;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 拼图填充规则服务测试
|
||||
*/
|
||||
@DisplayName("拼图填充规则服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PuzzleFillRuleServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private PuzzleFillRuleMapper ruleMapper;
|
||||
|
||||
@Mock
|
||||
private PuzzleFillRuleItemMapper itemMapper;
|
||||
|
||||
@InjectMocks
|
||||
private PuzzleFillRuleServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建规则应该保存主规则和明细")
|
||||
void shouldCreateRuleWithItems() {
|
||||
// Given
|
||||
PuzzleFillRuleSaveRequest request = createSaveRequest(null);
|
||||
request.setItems(Arrays.asList(
|
||||
createItemDTO("sfp_1", 1),
|
||||
createItemDTO("sfp_2", 2)
|
||||
));
|
||||
|
||||
// 模拟insert返回的ID
|
||||
doAnswer(invocation -> {
|
||||
PuzzleFillRuleEntity entity = invocation.getArgument(0);
|
||||
entity.setId(100L);
|
||||
return 1;
|
||||
}).when(ruleMapper).insert(any(PuzzleFillRuleEntity.class));
|
||||
|
||||
when(itemMapper.batchInsert(anyList())).thenReturn(2);
|
||||
|
||||
// When
|
||||
Long ruleId = service.create(request);
|
||||
|
||||
// Then
|
||||
assertEquals(100L, ruleId);
|
||||
|
||||
// 验证主规则保存
|
||||
verify(ruleMapper, times(1)).insert(any(PuzzleFillRuleEntity.class));
|
||||
|
||||
// 验证明细批量保存
|
||||
ArgumentCaptor<List<PuzzleFillRuleItemEntity>> itemsCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(itemMapper, times(1)).batchInsert(itemsCaptor.capture());
|
||||
|
||||
List<PuzzleFillRuleItemEntity> savedItems = itemsCaptor.getValue();
|
||||
assertEquals(2, savedItems.size());
|
||||
assertEquals(100L, savedItems.get(0).getRuleId());
|
||||
assertEquals(100L, savedItems.get(1).getRuleId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建规则时如果没有明细应该只保存主规则")
|
||||
void shouldCreateRuleWithoutItems() {
|
||||
// Given
|
||||
PuzzleFillRuleSaveRequest request = createSaveRequest(null);
|
||||
request.setItems(null);
|
||||
|
||||
doAnswer(invocation -> {
|
||||
PuzzleFillRuleEntity entity = invocation.getArgument(0);
|
||||
entity.setId(100L);
|
||||
return 1;
|
||||
}).when(ruleMapper).insert(any(PuzzleFillRuleEntity.class));
|
||||
|
||||
// When
|
||||
Long ruleId = service.create(request);
|
||||
|
||||
// Then
|
||||
assertEquals(100L, ruleId);
|
||||
verify(ruleMapper, times(1)).insert(any(PuzzleFillRuleEntity.class));
|
||||
verify(itemMapper, never()).batchInsert(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新规则应该先删除旧明细再插入新明细")
|
||||
void shouldUpdateRuleByDeletingOldItemsAndInsertingNew() {
|
||||
// Given
|
||||
PuzzleFillRuleSaveRequest request = createSaveRequest(10L);
|
||||
request.setItems(Arrays.asList(
|
||||
createItemDTO("sfp_1", 1),
|
||||
createItemDTO("sfp_2", 2)
|
||||
));
|
||||
|
||||
when(ruleMapper.updateById(any(PuzzleFillRuleEntity.class))).thenReturn(1);
|
||||
when(itemMapper.deleteByRuleId(10L)).thenReturn(2);
|
||||
when(itemMapper.batchInsert(anyList())).thenReturn(2);
|
||||
|
||||
// When
|
||||
Boolean result = service.update(request);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
|
||||
// 验证更新主规则
|
||||
verify(ruleMapper, times(1)).updateById(any(PuzzleFillRuleEntity.class));
|
||||
|
||||
// 验证删除旧明细
|
||||
verify(itemMapper, times(1)).deleteByRuleId(10L);
|
||||
|
||||
// 验证插入新明细
|
||||
verify(itemMapper, times(1)).batchInsert(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新规则时ID为null应该抛出异常")
|
||||
void shouldThrowExceptionWhenUpdateWithNullId() {
|
||||
// Given
|
||||
PuzzleFillRuleSaveRequest request = createSaveRequest(null);
|
||||
|
||||
// When & Then
|
||||
assertThrows(IllegalArgumentException.class, () -> service.update(request));
|
||||
verify(ruleMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除规则应该级联删除明细")
|
||||
void shouldDeleteRuleWithCascade() {
|
||||
// Given
|
||||
Long ruleId = 10L;
|
||||
when(ruleMapper.deleteById(ruleId)).thenReturn(1);
|
||||
|
||||
// When
|
||||
Boolean result = service.delete(ruleId);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
verify(ruleMapper, times(1)).deleteById(ruleId);
|
||||
// 明细由数据库外键级联删除,不需要手动删除
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询单条规则应该包含明细")
|
||||
void shouldGetRuleByIdWithItems() {
|
||||
// Given
|
||||
Long ruleId = 10L;
|
||||
|
||||
PuzzleFillRuleEntity ruleEntity = createRuleEntity(ruleId);
|
||||
when(ruleMapper.selectById(ruleId)).thenReturn(ruleEntity);
|
||||
|
||||
List<PuzzleFillRuleItemEntity> itemEntities = Arrays.asList(
|
||||
createItemEntity(1L, ruleId, "sfp_1"),
|
||||
createItemEntity(2L, ruleId, "sfp_2")
|
||||
);
|
||||
when(itemMapper.listByRuleId(ruleId)).thenReturn(itemEntities);
|
||||
|
||||
// When
|
||||
PuzzleFillRuleDTO dto = service.getById(ruleId);
|
||||
|
||||
// Then
|
||||
assertNotNull(dto);
|
||||
assertEquals(ruleId, dto.getId());
|
||||
assertEquals("测试规则", dto.getRuleName());
|
||||
assertNotNull(dto.getItems());
|
||||
assertEquals(2, dto.getItems().size());
|
||||
assertEquals("sfp_1", dto.getItems().get(0).getElementKey());
|
||||
assertEquals("sfp_2", dto.getItems().get(1).getElementKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询单条规则时规则不存在应该返回null")
|
||||
void shouldReturnNullWhenRuleNotFound() {
|
||||
// Given
|
||||
Long ruleId = 999L;
|
||||
when(ruleMapper.selectById(ruleId)).thenReturn(null);
|
||||
|
||||
// When
|
||||
PuzzleFillRuleDTO dto = service.getById(ruleId);
|
||||
|
||||
// Then
|
||||
assertNull(dto);
|
||||
verify(itemMapper, never()).listByRuleId(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询模板的所有规则应该包含明细")
|
||||
void shouldListRulesByTemplateIdWithItems() {
|
||||
// Given
|
||||
Long templateId = 1L;
|
||||
|
||||
List<PuzzleFillRuleEntity> ruleEntities = Arrays.asList(
|
||||
createRuleEntity(1L),
|
||||
createRuleEntity(2L)
|
||||
);
|
||||
when(ruleMapper.listByTemplateId(templateId)).thenReturn(ruleEntities);
|
||||
|
||||
when(itemMapper.listByRuleId(1L)).thenReturn(Arrays.asList(
|
||||
createItemEntity(1L, 1L, "sfp_1")
|
||||
));
|
||||
when(itemMapper.listByRuleId(2L)).thenReturn(Arrays.asList(
|
||||
createItemEntity(2L, 2L, "sfp_2")
|
||||
));
|
||||
|
||||
// When
|
||||
List<PuzzleFillRuleDTO> dtos = service.listByTemplateId(templateId);
|
||||
|
||||
// Then
|
||||
assertEquals(2, dtos.size());
|
||||
assertEquals(1L, dtos.get(0).getId());
|
||||
assertEquals(1, dtos.get(0).getItems().size());
|
||||
assertEquals(2L, dtos.get(1).getId());
|
||||
assertEquals(1, dtos.get(1).getItems().size());
|
||||
|
||||
verify(itemMapper, times(1)).listByRuleId(1L);
|
||||
verify(itemMapper, times(1)).listByRuleId(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("启用/禁用规则应该更新状态")
|
||||
void shouldToggleEnabled() {
|
||||
// Given
|
||||
Long ruleId = 10L;
|
||||
Integer enabled = 1;
|
||||
|
||||
when(ruleMapper.update(any(), any())).thenReturn(1);
|
||||
|
||||
// When
|
||||
Boolean result = service.toggleEnabled(ruleId, enabled);
|
||||
|
||||
// Then
|
||||
assertTrue(result);
|
||||
verify(ruleMapper, times(1)).update(any(), any());
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private PuzzleFillRuleSaveRequest createSaveRequest(Long id) {
|
||||
PuzzleFillRuleSaveRequest request = new PuzzleFillRuleSaveRequest();
|
||||
request.setId(id);
|
||||
request.setTemplateId(1L);
|
||||
request.setRuleName("测试规则");
|
||||
request.setConditionType("DEVICE_COUNT");
|
||||
request.setConditionValue("{\"deviceCount\": 4}");
|
||||
request.setPriority(100);
|
||||
request.setEnabled(1);
|
||||
request.setScenicId(1L);
|
||||
request.setDescription("测试描述");
|
||||
return request;
|
||||
}
|
||||
|
||||
private PuzzleFillRuleItemDTO createItemDTO(String elementKey, int order) {
|
||||
PuzzleFillRuleItemDTO dto = new PuzzleFillRuleItemDTO();
|
||||
dto.setElementKey(elementKey);
|
||||
dto.setDataSource("DEVICE_IMAGE");
|
||||
dto.setSourceFilter("{\"deviceIndex\": 0}");
|
||||
dto.setSortStrategy("LATEST");
|
||||
dto.setItemOrder(order);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private PuzzleFillRuleEntity createRuleEntity(Long id) {
|
||||
PuzzleFillRuleEntity entity = new PuzzleFillRuleEntity();
|
||||
entity.setId(id);
|
||||
entity.setTemplateId(1L);
|
||||
entity.setRuleName("测试规则");
|
||||
entity.setConditionType("DEVICE_COUNT");
|
||||
entity.setConditionValue("{\"deviceCount\": 4}");
|
||||
entity.setPriority(100);
|
||||
entity.setEnabled(1);
|
||||
entity.setScenicId(1L);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private PuzzleFillRuleItemEntity createItemEntity(Long id, Long ruleId, String elementKey) {
|
||||
PuzzleFillRuleItemEntity entity = new PuzzleFillRuleItemEntity();
|
||||
entity.setId(id);
|
||||
entity.setRuleId(ruleId);
|
||||
entity.setElementKey(elementKey);
|
||||
entity.setDataSource("DEVICE_IMAGE");
|
||||
entity.setSourceFilter("{\"deviceIndex\": 0}");
|
||||
entity.setSortStrategy("LATEST");
|
||||
entity.setItemOrder(id.intValue());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user