You've already forked FrameTour-BE
feat(puzzle): 实现智能自动填充引擎和安全增强
- 新增拼图元素自动填充引擎 PuzzleElementFillEngine - 支持基于规则的条件匹配和数据源解析 - 实现机位数量、机位ID等多维度条件策略 - 添加 DEVICE_IMAGE、USER_AVATAR 等数据源类型支持 - 增加景区隔离校验确保模板使用安全性 - 强化图片下载安全校验,防范 SSRF 攻击 - 支持本地文件路径解析和公网 URL 安全检查 - 完善静态值数据源策略支持 localPath 配置 - 优化生成流程中 faceId 和 scenicId 的校验逻辑 - 补充相关单元测试覆盖核心功能点
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user