refactor(puzzle): 重构元素DTO及新增元素基类

- 将ElementCreateRequest和PuzzleElementDTO中的elementType从Integer改为String
- 删除所有类型特定字段,新增config和configMap支持JSON配置
- 新增BaseElement抽象基类定义元素通用行为
- 添加ElementConfig接口和具体实现类ImageConfig、TextConfig
- 创建ElementFactory工厂类和ElementRegistrar注册器
- 新增ElementType枚举和ElementValidationException异常类
- 实现ImageElement和TextElement具体元素类
- 添加Position位置信息封装类
This commit is contained in:
2025-11-18 08:13:38 +08:00
parent 5c49a5af9e
commit 3d361200b0
28 changed files with 2988 additions and 615 deletions

View File

@@ -0,0 +1,219 @@
package com.ycwl.basic.puzzle.element.base;
import cn.hutool.core.util.StrUtil;
import com.ycwl.basic.puzzle.element.enums.ElementType;
import com.ycwl.basic.puzzle.element.exception.ElementValidationException;
import com.ycwl.basic.puzzle.element.renderer.RenderContext;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
/**
* 元素抽象基类
* 定义所有Element的通用行为和属性
*
* @author Claude
* @since 2025-01-18
*/
@Slf4j
@Data
public abstract class BaseElement {
/**
* 元素ID
*/
protected Long id;
/**
* 元素类型
*/
protected ElementType elementType;
/**
* 元素标识(用于动态数据映射)
*/
protected String elementKey;
/**
* 元素名称(便于管理识别)
*/
protected String elementName;
/**
* 位置信息
*/
protected Position position;
/**
* JSON配置字符串(原始)
*/
protected String configJson;
/**
* 解析后的配置对象(子类特定)
*/
protected ElementConfig config;
// ========== 抽象方法(子类必须实现) ==========
/**
* 加载并解析JSON配置
* 子类需要将configJson解析为具体的Config对象
*
* @param configJson JSON配置字符串
*/
public abstract void loadConfig(String configJson);
/**
* 验证元素配置是否合法
*
* @throws ElementValidationException 配置不合法时抛出
*/
public abstract void validate() throws ElementValidationException;
/**
* 渲染元素到画布
* 这是元素的核心方法,负责将元素绘制到Graphics2D上
*
* @param context 渲染上下文
*/
public abstract void render(RenderContext context);
/**
* 获取配置的JSON Schema或说明
*
* @return 配置说明
*/
public abstract String getConfigSchema();
// ========== 通用方法 ==========
/**
* 初始化元素(加载配置并验证)
* 在创建Element实例后必须调用此方法
*/
public void initialize() {
if (StrUtil.isNotBlank(configJson)) {
loadConfig(configJson);
}
validate();
}
/**
* 应用透明度
* 如果元素有透明度设置,则应用到Graphics2D上
*
* @param g2d Graphics2D对象
* @return 原始的Composite对象(用于恢复)
*/
protected Composite applyOpacity(Graphics2D g2d) {
Composite originalComposite = g2d.getComposite();
if (position != null && position.hasOpacity()) {
g2d.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
position.getOpacityFloat()
));
}
return originalComposite;
}
/**
* 恢复透明度
*
* @param g2d Graphics2D对象
* @param originalComposite 原始的Composite对象
*/
protected void restoreOpacity(Graphics2D g2d, Composite originalComposite) {
if (originalComposite != null) {
g2d.setComposite(originalComposite);
}
}
/**
* 应用旋转
* 如果元素有旋转设置,则应用到Graphics2D上
*
* @param g2d Graphics2D对象
*/
protected void applyRotation(Graphics2D g2d) {
if (position != null && position.hasRotation()) {
// 以元素中心点为旋转中心
int centerX = position.getX() + position.getWidth() / 2;
int centerY = position.getY() + position.getHeight() / 2;
g2d.rotate(position.getRotationRadians(), centerX, centerY);
}
}
/**
* 解析颜色字符串(支持hex格式)
*
* @param colorStr 颜色字符串(如#FFFFFF)
* @return Color对象
*/
protected Color parseColor(String colorStr) {
if (StrUtil.isBlank(colorStr)) {
return Color.BLACK;
}
try {
// 移除#号
String hex = colorStr.startsWith("#") ? colorStr.substring(1) : colorStr;
// 解析RGB
return new Color(
Integer.valueOf(hex.substring(0, 2), 16),
Integer.valueOf(hex.substring(2, 4), 16),
Integer.valueOf(hex.substring(4, 6), 16)
);
} catch (Exception e) {
log.warn("颜色解析失败: {}, 使用默认黑色", colorStr);
return Color.BLACK;
}
}
/**
* 安全解析JSON配置
*
* @param configJson JSON字符串
* @param configClass 配置类
* @param <T> 配置类型
* @return 配置对象
*/
protected <T extends ElementConfig> T parseConfig(String configJson, Class<T> configClass) {
try {
if (StrUtil.isBlank(configJson)) {
// 返回默认实例
return configClass.getDeclaredConstructor().newInstance();
}
return JacksonUtil.fromJson(configJson, configClass);
} catch (Exception e) {
throw new ElementValidationException(
elementType != null ? elementType.getCode() : "UNKNOWN",
elementKey,
"JSON配置解析失败: " + e.getMessage()
);
}
}
/**
* 启用高质量渲染
*
* @param g2d Graphics2D对象
*/
protected void enableHighQualityRendering(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
}
@Override
public String toString() {
return String.format("Element[type=%s, key=%s, name=%s, position=%s]",
elementType != null ? elementType.getCode() : "null",
elementKey,
elementName,
position);
}
}

View File

@@ -0,0 +1,30 @@
package com.ycwl.basic.puzzle.element.base;
/**
* 元素配置接口
* 所有Element的配置类都需要实现此接口
*
* @author Claude
* @since 2025-01-18
*/
public interface ElementConfig {
/**
* 获取配置说明(JSON Schema或描述)
*
* @return 配置说明
*/
default String getConfigSchema() {
return "{}";
}
/**
* 验证配置是否合法
* 子类应该重写此方法实现自己的验证逻辑
*
* @throws IllegalArgumentException 配置不合法时抛出
*/
default void validate() {
// 默认不做验证
}
}

View File

@@ -0,0 +1,172 @@
package com.ycwl.basic.puzzle.element.base;
import com.ycwl.basic.puzzle.element.enums.ElementType;
import com.ycwl.basic.puzzle.element.exception.ElementValidationException;
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 元素工厂类
* 负责根据类型创建Element实例
*
* @author Claude
* @since 2025-01-18
*/
@Slf4j
public class ElementFactory {
/**
* Element类型注册表
* key: ElementType枚举
* value: Element实现类的Class对象
*/
private static final Map<ElementType, Class<? extends BaseElement>> ELEMENT_REGISTRY = new ConcurrentHashMap<>();
/**
* 构造器缓存(性能优化)
* key: Element实现类
* value: 无参构造器
*/
private static final Map<Class<? extends BaseElement>, Constructor<? extends BaseElement>> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
/**
* 注册Element类型
*
* @param type 元素类型
* @param elementClass Element实现类
*/
public static void register(ElementType type, Class<? extends BaseElement> elementClass) {
if (type == null || elementClass == null) {
throw new IllegalArgumentException("注册参数不能为null");
}
ELEMENT_REGISTRY.put(type, elementClass);
log.info("注册Element类型: {} -> {}", type.getCode(), elementClass.getName());
}
/**
* 根据Entity创建Element实例
*
* @param entity PuzzleElementEntity
* @return Element实例
*/
public static BaseElement create(PuzzleElementEntity entity) {
if (entity == null) {
throw new IllegalArgumentException("Entity不能为null");
}
// 解析元素类型
ElementType type;
try {
type = ElementType.fromCode(entity.getElementType());
} catch (IllegalArgumentException e) {
throw new ElementValidationException(
entity.getElementType(),
entity.getElementKey(),
"未知的元素类型: " + entity.getElementType()
);
}
// 检查类型是否已实现
if (!type.isImplemented()) {
throw new ElementValidationException(
type.getCode(),
entity.getElementKey(),
"元素类型尚未实现: " + type.getName()
);
}
// 获取Element实现类
Class<? extends BaseElement> elementClass = ELEMENT_REGISTRY.get(type);
if (elementClass == null) {
throw new ElementValidationException(
type.getCode(),
entity.getElementKey(),
"元素类型未注册: " + type.getCode()
);
}
// 创建Element实例
BaseElement element = createInstance(elementClass);
// 填充基本属性
element.setId(entity.getId());
element.setElementType(type);
element.setElementKey(entity.getElementKey());
element.setElementName(entity.getElementName());
element.setConfigJson(entity.getConfig());
// 填充位置信息
Position position = new Position(
entity.getXPosition(),
entity.getYPosition(),
entity.getWidth(),
entity.getHeight(),
entity.getZIndex(),
entity.getRotation(),
entity.getOpacity()
);
element.setPosition(position);
// 初始化(加载配置并验证)
element.initialize();
log.debug("创建Element成功: type={}, key={}", type.getCode(), entity.getElementKey());
return element;
}
/**
* 创建Element实例(使用反射)
*
* @param elementClass Element类
* @return Element实例
*/
private static BaseElement createInstance(Class<? extends BaseElement> elementClass) {
try {
// 从缓存获取构造器
Constructor<? extends BaseElement> constructor = CONSTRUCTOR_CACHE.get(elementClass);
if (constructor == null) {
constructor = elementClass.getDeclaredConstructor();
constructor.setAccessible(true);
CONSTRUCTOR_CACHE.put(elementClass, constructor);
}
return constructor.newInstance();
} catch (Exception e) {
throw new ElementValidationException(
"Element实例创建失败: " + elementClass.getName() + ", 原因: " + e.getMessage(),
e
);
}
}
/**
* 获取已注册的Element类型列表
*
* @return Element类型列表
*/
public static Map<ElementType, Class<? extends BaseElement>> getRegisteredTypes() {
return new ConcurrentHashMap<>(ELEMENT_REGISTRY);
}
/**
* 检查类型是否已注册
*
* @param type 元素类型
* @return true-已注册,false-未注册
*/
public static boolean isRegistered(ElementType type) {
return ELEMENT_REGISTRY.containsKey(type);
}
/**
* 清空注册表(主要用于测试)
*/
public static void clearRegistry() {
ELEMENT_REGISTRY.clear();
CONSTRUCTOR_CACHE.clear();
log.warn("Element注册表已清空");
}
}

View File

@@ -0,0 +1,40 @@
package com.ycwl.basic.puzzle.element.base;
import com.ycwl.basic.puzzle.element.enums.ElementType;
import com.ycwl.basic.puzzle.element.impl.ImageElement;
import com.ycwl.basic.puzzle.element.impl.TextElement;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
/**
* Element注册器
* 在Spring容器初始化时自动注册所有Element类型
*
* @author Claude
* @since 2025-01-18
*/
@Slf4j
@Component
public class ElementRegistrar {
@PostConstruct
public void registerElements() {
log.info("开始注册Element类型...");
// 注册文字元素
ElementFactory.register(ElementType.TEXT, TextElement.class);
// 注册图片元素
ElementFactory.register(ElementType.IMAGE, ImageElement.class);
// 未来扩展的Element类型在这里注册
// ElementFactory.register(ElementType.QRCODE, QRCodeElement.class);
// ElementFactory.register(ElementType.GRADIENT, GradientElement.class);
// ElementFactory.register(ElementType.SHAPE, ShapeElement.class);
// ElementFactory.register(ElementType.DYNAMIC_IMAGE, DynamicImageElement.class);
log.info("Element类型注册完成,共注册{}种类型", ElementFactory.getRegisteredTypes().size());
}
}

View File

@@ -0,0 +1,95 @@
package com.ycwl.basic.puzzle.element.base;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 元素位置信息
* 封装所有与位置、大小、变换相关的属性
*
* @author Claude
* @since 2025-01-18
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Position {
/**
* X坐标(相对于画布左上角,像素)
*/
private Integer x;
/**
* Y坐标(相对于画布左上角,像素)
*/
private Integer y;
/**
* 宽度(像素)
*/
private Integer width;
/**
* 高度(像素)
*/
private Integer height;
/**
* 层级(数值越大越靠上,决定绘制顺序)
*/
private Integer zIndex;
/**
* 旋转角度(0-360度,顺时针)
*/
private Integer rotation;
/**
* 不透明度(0-100,100为完全不透明)
*/
private Integer opacity;
/**
* 获取不透明度的浮点数表示(0.0-1.0)
*
* @return 不透明度(0.0-1.0)
*/
public float getOpacityFloat() {
if (opacity == null) {
return 1.0f;
}
return Math.max(0, Math.min(100, opacity)) / 100.0f;
}
/**
* 获取旋转角度的弧度值
*
* @return 弧度值
*/
public double getRotationRadians() {
if (rotation == null || rotation == 0) {
return 0;
}
return Math.toRadians(rotation);
}
/**
* 是否需要旋转
*
* @return true-需要旋转,false-不需要
*/
public boolean hasRotation() {
return rotation != null && rotation != 0;
}
/**
* 是否有透明度
*
* @return true-有透明度,false-完全不透明
*/
public boolean hasOpacity() {
return opacity != null && opacity < 100;
}
}