You've already forked FrameTour-BE
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
// 默认不做验证
|
||||
}
|
||||
}
|
||||
@@ -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注册表已清空");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user