# 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, Map)`:主渲染方法 - 创建画布(根据模板宽高) - 绘制背景(纯色或图片背景) - 按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)`:批量添加元素 - `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` ```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 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