You've already forked FrameTour-BE
feat(puzzle): 实现拼图自动填充规则引擎及相关功能
- 新增拼图填充规则管理Controller、DTO、Entity等核心类 - 实现条件评估策略模式,支持多种匹配规则 - 实现数据源解析策略模式,支持多种数据来源 - 新增拼图元素自动填充引擎,支持优先级匹配和动态填充 - 在SourceMapper中增加设备统计和查询相关方法 - 在PuzzleGenerateRequest中新增faceId字段用于触发自动填充 - 完善相关枚举类和工具类,提升系统可维护性和扩展性
This commit is contained in:
@@ -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; // 默认返回最新
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user