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,611 @@
# Puzzle 拼图模块技术文档
## 📋 模块概述
Puzzle拼图模块是一个基于模板和元素的动态图片生成系统,支持按照预定义的模板配置,将动态数据渲染成最终的图片输出。常用于订单凭证、门票、证书等场景的图片生成。
**核心能力:**
- 模板化图片生成:通过模板+元素+动态数据生成定制化图片
- 多层次元素渲染:支持图片和文字元素的分层叠加
- 灵活的样式配置:支持位置、大小、透明度、旋转、圆角等属性
- 动态数据注入:通过elementKey进行动态数据替换
- 生成记录追踪:完整记录每次生成的参数和结果
**典型应用场景:**
- 订单凭证图片生成(用户头像+订单信息)
- 电子门票生成(二维码+用户信息)
- 电子证书生成(用户信息+证书模板)
- 营销海报生成(动态用户数据)
---
## 🏗️ 架构设计
### 分层结构
```
puzzle/
├── controller/ # API接口层
│ ├── PuzzleGenerateController.java # 拼图生成接口
│ └── PuzzleTemplateController.java # 模板管理接口
├── service/ # 业务逻辑层
│ ├── IPuzzleGenerateService.java
│ ├── IPuzzleTemplateService.java
│ └── impl/
│ ├── PuzzleGenerateServiceImpl.java
│ └── PuzzleTemplateServiceImpl.java
├── mapper/ # 数据访问层
│ ├── PuzzleTemplateMapper.java
│ ├── PuzzleElementMapper.java
│ └── PuzzleGenerationRecordMapper.java
├── entity/ # 实体类
│ ├── PuzzleTemplateEntity.java # 模板实体
│ ├── PuzzleElementEntity.java # 元素实体
│ └── PuzzleGenerationRecordEntity.java # 生成记录实体
├── dto/ # 数据传输对象
│ ├── PuzzleGenerateRequest.java # 生成请求
│ ├── PuzzleGenerateResponse.java # 生成响应
│ ├── PuzzleTemplateDTO.java # 模板DTO
│ ├── PuzzleElementDTO.java # 元素DTO
│ ├── TemplateCreateRequest.java # 模板创建请求
│ └── ElementCreateRequest.java # 元素创建请求
└── util/ # 工具类
└── PuzzleImageRenderer.java # 图片渲染引擎(核心)
```
### 设计模式
1. **服务层模式(Service Layer)**:业务逻辑封装在service层,controller只负责接口适配
2. **DTO模式**:使用独立的DTO对象处理API输入输出,与Entity分离
3. **策略模式**:图片适配模式(CONTAIN、COVER、FILL等)
4. **建造者模式**:通过模板+元素配置构建最终图片
---
## 🔧 核心组件详解
### 1. PuzzleImageRenderer - 图片渲染引擎
**职责**:核心渲染引擎,负责将模板配置和元素数据渲染成最终图片
**关键方法**
- `render(PuzzleTemplateEntity, List<PuzzleElementEntity>, Map<String, String>)`:主渲染方法
- 创建画布(根据模板宽高)
- 绘制背景(纯色或图片背景)
- 按z-index顺序绘制元素
- 返回BufferedImage对象
**渲染流程**
1. 创建画布:根据模板的canvasWidth和canvasHeight创建BufferedImage
2. 绘制背景:
- backgroundType=0:绘制纯色背景(backgroundColor)
- backgroundType=1:加载并绘制背景图片(backgroundImage)
3. 按z-index排序元素列表(升序,确保层级正确)
4. 逐个绘制元素:
- elementType=1(图片元素):
- 获取动态数据(dynamicData.get(elementKey))或使用defaultImageUrl
- 下载图片
- 根据imageFitMode缩放图片(CONTAIN/COVER/FILL/SCALE_DOWN)
- 应用borderRadius(圆角)
- 应用opacity(透明度)
- 应用rotation(旋转)
- 绘制到画布指定位置(xPosition, yPosition, width, height)
- elementType=2(文字元素):
- 获取动态数据或使用defaultText
- 设置字体(fontFamily, fontSize, fontWeight, fontStyle)
- 设置颜色(fontColor)
- 应用textAlign(对齐方式)
- 应用lineHeight(行高)
- 处理maxLines(最大行数截断)
- 应用textDecoration(下划线/删除线)
- 应用opacity和rotation
- 绘制到画布
**技术要点**
- 使用Java AWT进行图形绘制
- 使用Hutool工具库处理图片下载和基础操作
- 支持图片圆角(通过Ellipse2D.Float或RoundRectangle2D.Float实现clip)
- 支持透明度(通过AlphaComposite实现)
- 支持旋转(通过Graphics2D.rotate)
---
### 2. PuzzleGenerateServiceImpl - 拼图生成服务
**职责**:协调拼图生成的完整流程
**核心方法**
```java
PuzzleGenerateResponse generate(PuzzleGenerateRequest request)
```
**生成流程**
1. **参数校验**
- 校验templateCode是否提供
- 检查dynamicData是否为空
2. **加载模板**
- 根据templateCode查询模板(PuzzleTemplateMapper)
- 检查模板是否存在且启用(status=1)
- 检查多租户权限(scenicId匹配)
3. **加载元素**
- 根据templateId查询所有元素(PuzzleElementMapper)
- 按z-index升序排序
- 过滤未删除的元素(deleted=0)
4. **调用渲染引擎**
- 调用`PuzzleImageRenderer.render()`
- 传入模板、元素列表、动态数据
5. **上传图片**
- 将BufferedImage转换为字节流
- 估算文件大小
- 上传到对象存储(OSS)
- 获取图片URL
6. **创建生成记录**
- 保存到puzzle_generation_record表
- 记录参数、结果、耗时等信息
7. **返回响应**
- 返回图片URL、宽高、文件大小等信息
**辅助方法**
- `createRecord()`:创建生成记录
- `uploadImage()`:上传图片到OSS
- `estimateFileSize()`:估算文件大小
---
### 3. PuzzleTemplateServiceImpl - 模板管理服务
**职责**:管理拼图模板和元素的CRUD操作
**模板管理方法**
- `createTemplate(TemplateCreateRequest)`:创建模板
- `updateTemplate(Long, TemplateCreateRequest)`:更新模板
- `deleteTemplate(Long)`:逻辑删除模板(软删除)
- `getTemplateDetail(Long)`:查询模板详情(包含元素列表)
- `getTemplateByCode(String)`:根据code查询模板
- `listTemplates(Long, String, Integer)`:分页查询模板列表
**元素管理方法**
- `addElement(ElementCreateRequest)`:添加单个元素
- `batchAddElements(Long, List<ElementCreateRequest>)`:批量添加元素
- `updateElement(Long, ElementCreateRequest)`:更新元素
- `deleteElement(Long)`:逻辑删除元素
- `getElementDetail(Long)`:查询元素详情
**业务逻辑要点**
- 删除模板时会级联删除关联的所有元素
- 支持多租户隔离(根据scenicId)
- 支持按category分类查询
- 支持按status过滤启用/禁用的模板
---
### 4. Controller接口层
#### PuzzleGenerateController
```java
POST /puzzle/generate
```
**功能**:生成拼图图片
**请求体**`PuzzleGenerateRequest`
```json
{
"templateCode": "order_certificate",
"userId": 123,
"orderId": "ORDER20250117001",
"businessType": "order",
"scenicId": 1,
"dynamicData": {
"userAvatar": "https://example.com/avatar.jpg",
"userName": "张三",
"orderNumber": "ORDER20250117001",
"qrCode": "https://example.com/qr.png"
},
"outputFormat": "PNG",
"quality": 90
}
```
**响应**`AjaxResult<PuzzleGenerateResponse>`
```json
{
"code": 200,
"msg": "生成成功",
"data": {
"imageUrl": "https://oss.example.com/puzzle/xxx.png",
"width": 750,
"height": 1334,
"fileSize": 245678,
"recordId": 12345
}
}
```
#### PuzzleTemplateController
提供模板和元素的完整CRUD接口(详见Controller类)
---
## 📊 数据模型
### 1. puzzle_template - 拼图模板表
| 字段 | 类型 | 说明 |
|-----|------|-----|
| id | BIGINT | 主键ID |
| name | VARCHAR(100) | 模板名称 |
| code | VARCHAR(50) | 模板编码(唯一,用于API调用) |
| canvas_width | INT | 画布宽度(像素) |
| canvas_height | INT | 画布高度(像素) |
| background_type | TINYINT | 背景类型:0-纯色 1-图片 |
| background_color | VARCHAR(20) | 背景颜色(hex格式,如#FFFFFF) |
| background_image | VARCHAR(500) | 背景图片URL |
| description | TEXT | 模板描述 |
| category | VARCHAR(50) | 模板分类(order/ticket/certificate等) |
| status | TINYINT | 状态:0-禁用 1-启用 |
| scenic_id | BIGINT | 景区ID(多租户隔离) |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
| deleted | TINYINT | 删除标记:0-未删除 1-已删除 |
| deleted_at | DATETIME | 删除时间 |
**索引**
- UNIQUE KEY `uk_code` (code, deleted)
- KEY `idx_scenic_id` (scenic_id)
- KEY `idx_category_status` (category, status, deleted)
---
### 2. puzzle_element - 拼图元素表
| 字段 | 类型 | 说明 |
|-----|------|-----|
| id | BIGINT | 主键ID |
| template_id | BIGINT | 模板ID(外键) |
| element_type | TINYINT | 元素类型:1-图片 2-文字 |
| element_key | VARCHAR(50) | 元素标识(用于动态数据映射) |
| element_name | VARCHAR(100) | 元素名称(便于管理) |
**位置和布局属性**
| 字段 | 类型 | 说明 |
|-----|------|-----|
| x_position | INT | X坐标(相对画布左上角) |
| y_position | INT | Y坐标(相对画布左上角) |
| width | INT | 宽度(像素) |
| height | INT | 高度(像素) |
| z_index | INT | 层级(数值越大越靠上) |
| rotation | INT | 旋转角度(0-360度,顺时针) |
| opacity | INT | 不透明度(0-100) |
**图片元素专有属性**
| 字段 | 类型 | 说明 |
|-----|------|-----|
| default_image_url | VARCHAR(500) | 默认图片URL |
| image_fit_mode | VARCHAR(20) | 图片适配模式:CONTAIN/COVER/FILL/SCALE_DOWN |
| border_radius | INT | 圆角半径(像素) |
**文字元素专有属性**
| 字段 | 类型 | 说明 |
|-----|------|-----|
| default_text | TEXT | 默认文本内容 |
| font_family | VARCHAR(50) | 字体名称 |
| font_size | INT | 字号(像素) |
| font_color | VARCHAR(20) | 字体颜色(hex) |
| font_weight | VARCHAR(20) | 字重:NORMAL/BOLD |
| font_style | VARCHAR(20) | 字体样式:NORMAL/ITALIC |
| text_align | VARCHAR(20) | 对齐方式:LEFT/CENTER/RIGHT |
| line_height | DECIMAL(3,2) | 行高倍数(如1.5) |
| max_lines | INT | 最大行数(NULL表示不限制) |
| text_decoration | VARCHAR(20) | 文本装饰:NONE/UNDERLINE/LINE_THROUGH |
**索引**
- KEY `idx_template_id` (template_id, deleted)
- KEY `idx_element_key` (element_key)
---
### 3. puzzle_generation_record - 拼图生成记录表
| 字段 | 类型 | 说明 |
|-----|------|-----|
| id | BIGINT | 主键ID |
| template_id | BIGINT | 模板ID |
| template_code | VARCHAR(50) | 模板编码(冗余) |
| user_id | BIGINT | 用户ID |
| order_id | VARCHAR(50) | 关联订单号 |
| business_type | VARCHAR(50) | 业务类型 |
| generation_params | TEXT | 生成参数(JSON格式) |
| result_image_url | VARCHAR(500) | 生成的图片URL |
| result_file_size | BIGINT | 文件大小(字节) |
| result_width | INT | 图片宽度 |
| result_height | INT | 图片高度 |
| status | TINYINT | 状态:0-生成中 1-成功 2-失败 |
| error_message | TEXT | 错误信息(失败时) |
| generation_duration | INT | 生成耗时(毫秒) |
| retry_count | INT | 重试次数 |
| scenic_id | BIGINT | 景区ID |
| client_ip | VARCHAR(50) | 客户端IP |
| user_agent | VARCHAR(500) | 客户端User-Agent |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
**索引**
- KEY `idx_user_id` (user_id)
- KEY `idx_order_id` (order_id)
- KEY `idx_template_id` (template_id)
- KEY `idx_create_time` (create_time)
---
## 🔄 关键业务流程
### 拼图生成完整流程
```
用户请求 → Controller接收
验证templateCode和dynamicData
根据templateCode查询模板(含权限校验)
根据templateId查询所有元素(按z-index排序)
调用PuzzleImageRenderer.render()
├─ 创建画布
├─ 绘制背景
├─ 遍历元素列表
│ ├─ 图片元素:下载图片 → 缩放/圆角/透明度/旋转 → 绘制
│ └─ 文字元素:设置字体样式 → 计算布局 → 绘制
└─ 返回BufferedImage
将BufferedImage转换为字节流
上传到OSS获取URL
创建生成记录(保存参数和结果)
返回响应(imageUrl、width、height、fileSize等)
```
### 图片适配模式说明
**CONTAIN(等比缩放适应)**
- 图片完全显示在区域内
- 保持图片宽高比
- 可能留白
**COVER(等比缩放填充)**
- 完全覆盖目标区域
- 保持图片宽高比
- 可能裁剪图片
**FILL(拉伸填充)**
- 完全填充目标区域
- 不保持宽高比
- 可能变形
**SCALE_DOWN(缩小适应)**
- 类似CONTAIN
- 但不放大图片(仅缩小)
---
## 🛠️ 技术栈
### 核心依赖
- **Spring Boot**:框架基础
- **MyBatis Plus**:数据访问
- **Lombok**:减少样板代码
- **Hutool**:工具类库(图片处理、HTTP下载)
- **Java AWT/ImageIO**:图形绘制和图片处理
- **SLF4J/Logback**:日志
### 外部依赖
- **OSS对象存储**:图片上传和存储
- **MySQL**:关系型数据库
---
## 📦 对外依赖
puzzle模块与其他模块的依赖关系:
| 依赖模块 | 依赖项 | 用途 |
|---------|--------|-----|
| storage | OSS上传服务 | 上传生成的图片到对象存储 |
| config | 全局配置 | 获取系统配置信息 |
| exception | 自定义异常 | 业务异常处理 |
| utils | 工具类 | 通用工具方法 |
**被依赖情况**
- order模块:订单凭证图片生成
- ticket模块:电子门票图片生成
- 其他需要动态图片生成的业务模块
---
## 🚀 扩展指南
### 1. 新增元素类型
如需支持新的元素类型(如二维码元素、形状元素等):
1.`PuzzleElementEntity`中新增`elementType`枚举值
2.`PuzzleImageRenderer.render()`中添加新类型的渲染逻辑
3. 新增元素专有属性到`puzzle_element`表和实体类
4. 更新DTO和请求对象
### 2. 新增图片格式支持
当前支持PNG和JPEG,如需支持WebP、SVG等:
1. 更新`PuzzleGenerateRequest.outputFormat`校验逻辑
2. 修改`PuzzleGenerateServiceImpl.uploadImage()`中的格式转换逻辑
3. 注意浏览器兼容性
### 3. 新增渲染效果
如需支持阴影、边框、渐变等效果:
1.`PuzzleElementEntity`中新增对应的属性字段
2.`PuzzleImageRenderer`中实现对应的绘制逻辑
3. 使用Java AWT的相关API(如`setShadow``drawRect`等)
### 4. 批量生成优化
如需支持批量生成(如批量生成门票):
1. 新增批量生成接口`POST /puzzle/batchGenerate`
2. 使用线程池并发处理
3. 返回任务ID,支持异步查询结果
---
## ⚠️ 重要注意事项
### 1. 性能优化
- **图片缓存**:对于默认图片URL,考虑使用本地缓存避免重复下载
- **并发控制**:高并发场景下,生成接口应加限流保护
- **资源释放**:及时释放BufferedImage和Graphics2D对象,避免内存泄漏
- **异步处理**:对于复杂模板,考虑异步生成+回调通知
### 2. 安全性
- **URL校验**:对dynamicData中的图片URL进行白名单校验,防止SSRF攻击
- **文件大小限制**:限制下载图片的大小,防止资源耗尽
- **权限控制**:确保scenicId隔离,防止越权访问
- **输入校验**:严格校验所有输入参数,防止XSS和注入攻击
### 3. 多租户隔离
- 所有查询必须带上scenicId条件
- 创建模板和元素时必须关联正确的scenicId
- 生成拼图时校验模板的scenicId权限
### 4. 错误处理
- 图片下载失败时的降级策略(使用默认图片)
- 渲染失败时记录详细错误日志
- 对外暴露友好的错误提示
### 5. 字体问题
- **字体文件**:确保服务器安装了模板使用的字体文件
- **中文字体**:Linux服务器需要安装中文字体(如文泉驿)
- **字体回退**:设置字体回退机制,避免乱码
### 6. 数据一致性
- 删除模板时级联删除元素(软删除)
- 更新模板状态时考虑对正在生成的任务的影响
- 生成记录不可删除,仅供审计和统计
### 7. 监控和日志
- 记录每次生成的耗时,监控性能
- 记录生成失败的详细原因,便于排查
- 统计各模板的使用频率,优化热点模板
---
## 📈 性能指标参考
**典型性能数据**(测试环境):
- 单张简单拼图(2-3个元素):< 500ms
- 单张复杂拼图10+个元素):< 1500ms
- 图片下载耗时200-500ms取决于网络
- 渲染耗时50-200ms
- OSS上传耗时100-300ms
**优化建议**
- 使用CDN加速图片下载
- 预热常用模板的背景图片
- 使用Redis缓存模板和元素配置
---
## 📝 示例代码
### 创建模板示例
```java
// 1. 创建模板
TemplateCreateRequest templateReq = new TemplateCreateRequest();
templateReq.setName("订单凭证模板");
templateReq.setCode("order_certificate_v1");
templateReq.setCanvasWidth(750);
templateReq.setCanvasHeight(1334);
templateReq.setBackgroundType(1);
templateReq.setBackgroundImage("https://oss.example.com/bg.jpg");
templateReq.setCategory("order");
templateReq.setScenicId(1L);
Long templateId = templateService.createTemplate(templateReq);
// 2. 添加元素 - 用户头像(图片元素)
ElementCreateRequest avatarElement = new ElementCreateRequest();
avatarElement.setTemplateId(templateId);
avatarElement.setElementType(1); // 图片
avatarElement.setElementKey("userAvatar");
avatarElement.setElementName("用户头像");
avatarElement.setXPosition(50);
avatarElement.setYPosition(100);
avatarElement.setWidth(100);
avatarElement.setHeight(100);
avatarElement.setZIndex(10);
avatarElement.setDefaultImageUrl("https://oss.example.com/default-avatar.png");
avatarElement.setImageFitMode("COVER");
avatarElement.setBorderRadius(50); // 圆形头像
avatarElement.setOpacity(100);
templateService.addElement(avatarElement);
// 3. 添加元素 - 用户名(文字元素)
ElementCreateRequest nameElement = new ElementCreateRequest();
nameElement.setTemplateId(templateId);
nameElement.setElementType(2); // 文字
nameElement.setElementKey("userName");
nameElement.setElementName("用户名");
nameElement.setXPosition(170);
nameElement.setYPosition(120);
nameElement.setWidth(300);
nameElement.setHeight(60);
nameElement.setZIndex(20);
nameElement.setDefaultText("用户名");
nameElement.setFontFamily("PingFang SC");
nameElement.setFontSize(28);
nameElement.setFontColor("#333333");
nameElement.setFontWeight("BOLD");
nameElement.setTextAlign("LEFT");
nameElement.setLineHeight(new BigDecimal("1.5"));
templateService.addElement(nameElement);
```
### 生成拼图示例
```java
// 调用生成接口
PuzzleGenerateRequest request = new PuzzleGenerateRequest();
request.setTemplateCode("order_certificate_v1");
request.setUserId(123L);
request.setOrderId("ORDER20250117001");
request.setBusinessType("order");
request.setScenicId(1L);
Map<String, String> dynamicData = new HashMap<>();
dynamicData.put("userAvatar", "https://oss.example.com/user123/avatar.jpg");
dynamicData.put("userName", "张三");
dynamicData.put("orderNumber", "ORDER20250117001");
dynamicData.put("qrCode", "https://oss.example.com/qr/ORDER20250117001.png");
request.setDynamicData(dynamicData);
request.setOutputFormat("PNG");
request.setQuality(90);
PuzzleGenerateResponse response = generateService.generate(request);
System.out.println("生成成功,图片URL: " + response.getImageUrl());
```
---
## 🔗 相关文档
- [MyBatis-Plus官方文档](https://baomidou.com/)
- [Hutool工具类文档](https://hutool.cn/)
- [Java AWT图形绘制教程](https://docs.oracle.com/javase/tutorial/2d/)
- [阿里云OSS Java SDK](https://help.aliyun.com/document_detail/32008.html)
---
## 📞 联系方式
如有问题或建议请联系模块负责人或提交Issue
**维护者**Claude
**创建时间**2025-01-17
**最后更新**2025-01-17