You've already forked FrameTour-BE
- 将ElementCreateRequest和PuzzleElementDTO中的elementType从Integer改为String - 删除所有类型特定字段,新增config和configMap支持JSON配置 - 新增BaseElement抽象基类定义元素通用行为 - 添加ElementConfig接口和具体实现类ImageConfig、TextConfig - 创建ElementFactory工厂类和ElementRegistrar注册器 - 新增ElementType枚举和ElementValidationException异常类 - 实现ImageElement和TextElement具体元素类 - 添加Position位置信息封装类
20 KiB
20 KiB
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 # 图片渲染引擎(核心)
设计模式
- 服务层模式(Service Layer):业务逻辑封装在service层,controller只负责接口适配
- DTO模式:使用独立的DTO对象处理API输入输出,与Entity分离
- 策略模式:图片适配模式(CONTAIN、COVER、FILL等)
- 建造者模式:通过模板+元素配置构建最终图片
🔧 核心组件详解
1. PuzzleImageRenderer - 图片渲染引擎
职责:核心渲染引擎,负责将模板配置和元素数据渲染成最终图片
关键方法:
render(PuzzleTemplateEntity, List<PuzzleElementEntity>, Map<String, String>):主渲染方法- 创建画布(根据模板宽高)
- 绘制背景(纯色或图片背景)
- 按z-index顺序绘制元素
- 返回BufferedImage对象
渲染流程:
- 创建画布:根据模板的canvasWidth和canvasHeight创建BufferedImage
- 绘制背景:
- backgroundType=0:绘制纯色背景(backgroundColor)
- backgroundType=1:加载并绘制背景图片(backgroundImage)
- 按z-index排序元素列表(升序,确保层级正确)
- 逐个绘制元素:
- 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
- 绘制到画布
- elementType=1(图片元素):
技术要点:
- 使用Java AWT进行图形绘制
- 使用Hutool工具库处理图片下载和基础操作
- 支持图片圆角(通过Ellipse2D.Float或RoundRectangle2D.Float实现clip)
- 支持透明度(通过AlphaComposite实现)
- 支持旋转(通过Graphics2D.rotate)
2. PuzzleGenerateServiceImpl - 拼图生成服务
职责:协调拼图生成的完整流程
核心方法:
PuzzleGenerateResponse generate(PuzzleGenerateRequest request)
生成流程:
- 参数校验:
- 校验templateCode是否提供
- 检查dynamicData是否为空
- 加载模板:
- 根据templateCode查询模板(PuzzleTemplateMapper)
- 检查模板是否存在且启用(status=1)
- 检查多租户权限(scenicId匹配)
- 加载元素:
- 根据templateId查询所有元素(PuzzleElementMapper)
- 按z-index升序排序
- 过滤未删除的元素(deleted=0)
- 调用渲染引擎:
- 调用
PuzzleImageRenderer.render() - 传入模板、元素列表、动态数据
- 调用
- 上传图片:
- 将BufferedImage转换为字节流
- 估算文件大小
- 上传到对象存储(OSS)
- 获取图片URL
- 创建生成记录:
- 保存到puzzle_generation_record表
- 记录参数、结果、耗时等信息
- 返回响应:
- 返回图片URL、宽高、文件大小等信息
辅助方法:
createRecord():创建生成记录uploadImage():上传图片到OSSestimateFileSize():估算文件大小
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
POST /puzzle/generate
功能:生成拼图图片
请求体:PuzzleGenerateRequest
{
"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>
{
"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. 新增元素类型
如需支持新的元素类型(如二维码元素、形状元素等):
- 在
PuzzleElementEntity中新增elementType枚举值 - 在
PuzzleImageRenderer.render()中添加新类型的渲染逻辑 - 新增元素专有属性到
puzzle_element表和实体类 - 更新DTO和请求对象
2. 新增图片格式支持
当前支持PNG和JPEG,如需支持WebP、SVG等:
- 更新
PuzzleGenerateRequest.outputFormat校验逻辑 - 修改
PuzzleGenerateServiceImpl.uploadImage()中的格式转换逻辑 - 注意浏览器兼容性
3. 新增渲染效果
如需支持阴影、边框、渐变等效果:
- 在
PuzzleElementEntity中新增对应的属性字段 - 在
PuzzleImageRenderer中实现对应的绘制逻辑 - 使用Java AWT的相关API(如
setShadow、drawRect等)
4. 批量生成优化
如需支持批量生成(如批量生成门票):
- 新增批量生成接口
POST /puzzle/batchGenerate - 使用线程池并发处理
- 返回任务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缓存模板和元素配置
📝 示例代码
创建模板示例
// 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);
生成拼图示例
// 调用生成接口
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());
🔗 相关文档
📞 联系方式
如有问题或建议,请联系模块负责人或提交Issue。
维护者:Claude 创建时间:2025-01-17 最后更新:2025-01-17