feat(puzzle): 实现智能自动填充引擎和安全增强

- 新增拼图元素自动填充引擎 PuzzleElementFillEngine
- 支持基于规则的条件匹配和数据源解析
- 实现机位数量、机位ID等多维度条件策略
- 添加 DEVICE_IMAGE、USER_AVATAR 等数据源类型支持
- 增加景区隔离校验确保模板使用安全性
- 强化图片下载安全校验,防范 SSRF 攻击
- 支持本地文件路径解析和公网 URL 安全检查
- 完善静态值数据源策略支持 localPath 配置
- 优化生成流程中 faceId 和 scenicId 的校验逻辑
- 补充相关单元测试覆盖核心功能点
This commit is contained in:
2025-11-19 17:28:41 +08:00
parent cb17ea527b
commit cfb3625ac0
12 changed files with 748 additions and 57 deletions

View File

@@ -62,20 +62,23 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
throw new IllegalArgumentException("模板已禁用: " + request.getTemplateCode());
}
// 2. 校验景区隔离
Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(template.getId());
if (elements.isEmpty()) {
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
}
// 2. 按z-index排序元素
// 3. 按z-index排序元素
elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
Comparator.nullsFirst(Comparator.naturalOrder())));
// 3. 准备dynamicData(合并自动填充和手动数据)
Map<String, String> finalDynamicData = buildDynamicData(template, request);
// 4. 准备dynamicData(合并自动填充和手动数据)
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId);
// 4. 创建生成记录
PuzzleGenerationRecordEntity record = createRecord(template, request);
// 5. 创建生成记录
PuzzleGenerationRecordEntity record = createRecord(template, request, resolvedScenicId);
recordMapper.insert(record);
try {
@@ -121,14 +124,16 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
/**
* 创建生成记录
*/
private PuzzleGenerationRecordEntity createRecord(PuzzleTemplateEntity template, PuzzleGenerateRequest request) {
private PuzzleGenerationRecordEntity createRecord(PuzzleTemplateEntity template,
PuzzleGenerateRequest request,
Long scenicId) {
PuzzleGenerationRecordEntity record = new PuzzleGenerationRecordEntity();
record.setTemplateId(template.getId());
record.setTemplateCode(template.getCode());
record.setUserId(request.getUserId());
record.setOrderId(request.getOrderId());
record.setBusinessType(request.getBusinessType());
record.setScenicId(request.getScenicId());
record.setScenicId(scenicId);
record.setStatus(0); // 生成中
record.setRetryCount(0);
@@ -191,16 +196,18 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
* 构建dynamicData(合并自动填充和手动数据)
* 优先级: 手动传入的数据 > 自动填充的数据
*/
private Map<String, String> buildDynamicData(PuzzleTemplateEntity template, PuzzleGenerateRequest request) {
private Map<String, String> buildDynamicData(PuzzleTemplateEntity template,
PuzzleGenerateRequest request,
Long scenicId) {
Map<String, String> dynamicData = new HashMap<>();
// 1. 自动填充(基于faceId和规则)
if (request.getFaceId() != null && request.getScenicId() != null) {
if (request.getFaceId() != null && scenicId != null) {
try {
Map<String, String> autoFilled = fillEngine.execute(
template.getId(),
request.getFaceId(),
request.getScenicId()
scenicId
);
if (autoFilled != null && !autoFilled.isEmpty()) {
dynamicData.putAll(autoFilled);
@@ -210,6 +217,9 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
log.error("自动填充异常, templateId={}, faceId={}", template.getId(), request.getFaceId(), e);
// 自动填充失败不影响整体流程,继续执行
}
} else if (request.getFaceId() != null) {
log.warn("自动填充被跳过: 缺少scenicId或模板未绑定景区, templateId={}, faceId={}",
template.getId(), request.getFaceId());
}
// 2. 手动数据覆盖(优先级更高)
@@ -221,4 +231,28 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
log.info("最终dynamicData: {}", dynamicData.keySet());
return dynamicData;
}
/**
* 校验模板与请求景区的合法性
*
* @param template 模板
* @param requestedScenic 请求中的景区ID
* @return 最终生效的景区ID
*/
private Long resolveScenicId(PuzzleTemplateEntity template, Long requestedScenic) {
Long templateScenicId = template.getScenicId();
if (templateScenicId == null) {
return requestedScenic;
}
if (requestedScenic == null) {
throw new IllegalArgumentException("模板绑定景区, scenicId为必填项");
}
if (!templateScenicId.equals(requestedScenic)) {
throw new IllegalArgumentException("模板不属于当前景区, 请检查templateCode与scenicId");
}
return templateScenicId;
}
}