feat(puzzle): 实现拼图自动填充规则引擎及相关功能

- 新增拼图填充规则管理Controller、DTO、Entity等核心类
- 实现条件评估策略模式,支持多种匹配规则
- 实现数据源解析策略模式,支持多种数据来源
- 新增拼图元素自动填充引擎,支持优先级匹配和动态填充
- 在SourceMapper中增加设备统计和查询相关方法
- 在PuzzleGenerateRequest中新增faceId字段用于触发自动填充
- 完善相关枚举类和工具类,提升系统可维护性和扩展性
This commit is contained in:
2025-11-19 11:10:23 +08:00
parent de421cf0d5
commit 778afaaa83
43 changed files with 4019 additions and 3 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}