You've already forked FrameTour-BE
feat(puzzle): 实现拼图生成功能模块
- 新增拼图生成控制器 PuzzleGenerateController,支持 /api/puzzle/generate 接口 - 新增拼图模板管理控制器 PuzzleTemplateController,提供完整的 CRUD 和元素管理功能 - 定义拼图相关 DTO 类,包括模板、元素、生成请求与响应等数据传输对象 - 创建拼图相关的实体类 PuzzleTemplateEntity、PuzzleElementEntity 和 PuzzleGenerationRecordEntity - 实现 Mapper 接口用于数据库操作,支持模板和元素的增删改查及生成记录管理 - 开发 PuzzleGenerateServiceImpl 服务,完成从模板渲染到图片上传的完整流程 - 提供 PuzzleTemplateServiceImpl 服务,实现模板及其元素的全生命周期管理 - 引入 PuzzleImageRenderer 工具类负责图像合成渲染逻辑 - 支持将生成结果上传至 OSS 并记录生成过程的日志和元数据
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.ycwl.basic.puzzle.controller;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 拼图生成Controller(C端API)
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/puzzle")
|
||||
@RequiredArgsConstructor
|
||||
public class PuzzleGenerateController {
|
||||
|
||||
private final IPuzzleGenerateService generateService;
|
||||
|
||||
/**
|
||||
* 生成拼图图片
|
||||
*/
|
||||
@PostMapping("/generate")
|
||||
public ApiResponse<PuzzleGenerateResponse> generatePuzzle(@RequestBody PuzzleGenerateRequest request) {
|
||||
log.info("拼图生成请求: templateCode={}, userId={}, orderId={}",
|
||||
request.getTemplateCode(), request.getUserId(), request.getOrderId());
|
||||
|
||||
// 参数校验
|
||||
if (request.getTemplateCode() == null || request.getTemplateCode().trim().isEmpty()) {
|
||||
return ApiResponse.fail("模板编码不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
PuzzleGenerateResponse response = generateService.generate(request);
|
||||
return ApiResponse.success(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("拼图生成参数错误: {}", e.getMessage());
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("拼图生成失败", e);
|
||||
return ApiResponse.fail("图片生成失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.ycwl.basic.puzzle.controller;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.ElementCreateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleElementDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||
import com.ycwl.basic.puzzle.dto.TemplateCreateRequest;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图模板管理Controller(管理后台)
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/puzzle/admin")
|
||||
@RequiredArgsConstructor
|
||||
public class PuzzleTemplateController {
|
||||
|
||||
private final IPuzzleTemplateService templateService;
|
||||
|
||||
/**
|
||||
* 创建模板
|
||||
*/
|
||||
@PostMapping("/templates")
|
||||
public ApiResponse<Long> createTemplate(@RequestBody TemplateCreateRequest request) {
|
||||
log.info("创建模板请求: code={}, name={}", request.getCode(), request.getName());
|
||||
Long templateId = templateService.createTemplate(request);
|
||||
return ApiResponse.success(templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模板
|
||||
*/
|
||||
@PutMapping("/templates/{id}")
|
||||
public ApiResponse<Void> updateTemplate(@PathVariable Long id,
|
||||
@RequestBody TemplateCreateRequest request) {
|
||||
log.info("更新模板请求: id={}", id);
|
||||
templateService.updateTemplate(id, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板
|
||||
*/
|
||||
@DeleteMapping("/templates/{id}")
|
||||
public ApiResponse<Void> deleteTemplate(@PathVariable Long id) {
|
||||
log.info("删除模板请求: id={}", id);
|
||||
templateService.deleteTemplate(id);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板详情
|
||||
*/
|
||||
@GetMapping("/templates/{id}")
|
||||
public ApiResponse<PuzzleTemplateDTO> getTemplateDetail(@PathVariable Long id) {
|
||||
log.debug("获取模板详情: id={}", id);
|
||||
PuzzleTemplateDTO template = templateService.getTemplateDetail(id);
|
||||
return ApiResponse.success(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板列表
|
||||
*/
|
||||
@GetMapping("/templates")
|
||||
public ApiResponse<List<PuzzleTemplateDTO>> listTemplates(
|
||||
@RequestParam(required = false) Long scenicId,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
log.debug("查询模板列表: scenicId={}, category={}, status={}", scenicId, category, status);
|
||||
List<PuzzleTemplateDTO> templates = templateService.listTemplates(scenicId, category, status);
|
||||
return ApiResponse.success(templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为模板添加单个元素
|
||||
*/
|
||||
@PostMapping("/elements")
|
||||
public ApiResponse<Long> addElement(@RequestBody ElementCreateRequest request) {
|
||||
log.info("添加元素请求: templateId={}, elementKey={}", request.getTemplateId(), request.getElementKey());
|
||||
Long elementId = templateService.addElement(request);
|
||||
return ApiResponse.success(elementId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为模板批量添加元素
|
||||
*/
|
||||
@PostMapping("/templates/{templateId}/elements")
|
||||
public ApiResponse<Void> batchAddElements(@PathVariable Long templateId,
|
||||
@RequestBody List<ElementCreateRequest> elements) {
|
||||
log.info("批量添加元素请求: templateId={}, count={}", templateId, elements.size());
|
||||
templateService.batchAddElements(templateId, elements);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元素
|
||||
*/
|
||||
@PutMapping("/elements/{id}")
|
||||
public ApiResponse<Void> updateElement(@PathVariable Long id,
|
||||
@RequestBody ElementCreateRequest request) {
|
||||
log.info("更新元素请求: id={}", id);
|
||||
templateService.updateElement(id, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除元素
|
||||
*/
|
||||
@DeleteMapping("/elements/{id}")
|
||||
public ApiResponse<Void> deleteElement(@PathVariable Long id) {
|
||||
log.info("删除元素请求: id={}", id);
|
||||
templateService.deleteElement(id);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素详情
|
||||
*/
|
||||
@GetMapping("/elements/{id}")
|
||||
public ApiResponse<PuzzleElementDTO> getElementDetail(@PathVariable Long id) {
|
||||
log.debug("获取元素详情: id={}", id);
|
||||
PuzzleElementDTO element = templateService.getElementDetail(id);
|
||||
return ApiResponse.success(element);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 创建元素请求DTO
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
public class ElementCreateRequest {
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 元素类型:1-图片 2-文字
|
||||
*/
|
||||
private Integer elementType;
|
||||
|
||||
/**
|
||||
* 元素标识
|
||||
*/
|
||||
private String elementKey;
|
||||
|
||||
/**
|
||||
* 元素名称
|
||||
*/
|
||||
private String elementName;
|
||||
|
||||
// ===== 位置和布局属性 =====
|
||||
|
||||
/**
|
||||
* X坐标
|
||||
*/
|
||||
private Integer xPosition;
|
||||
|
||||
/**
|
||||
* Y坐标
|
||||
*/
|
||||
private Integer yPosition;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 层级
|
||||
*/
|
||||
private Integer zIndex;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
private Integer rotation;
|
||||
|
||||
/**
|
||||
* 不透明度
|
||||
*/
|
||||
private Integer opacity;
|
||||
|
||||
// ===== 图片元素属性 =====
|
||||
|
||||
/**
|
||||
* 默认图片URL
|
||||
*/
|
||||
private String defaultImageUrl;
|
||||
|
||||
/**
|
||||
* 图片适配模式
|
||||
*/
|
||||
private String imageFitMode;
|
||||
|
||||
/**
|
||||
* 圆角半径
|
||||
*/
|
||||
private Integer borderRadius;
|
||||
|
||||
// ===== 文字元素属性 =====
|
||||
|
||||
/**
|
||||
* 默认文本内容
|
||||
*/
|
||||
private String defaultText;
|
||||
|
||||
/**
|
||||
* 字体
|
||||
*/
|
||||
private String fontFamily;
|
||||
|
||||
/**
|
||||
* 字号
|
||||
*/
|
||||
private Integer fontSize;
|
||||
|
||||
/**
|
||||
* 字体颜色
|
||||
*/
|
||||
private String fontColor;
|
||||
|
||||
/**
|
||||
* 字重
|
||||
*/
|
||||
private String fontWeight;
|
||||
|
||||
/**
|
||||
* 字体样式
|
||||
*/
|
||||
private String fontStyle;
|
||||
|
||||
/**
|
||||
* 对齐方式
|
||||
*/
|
||||
private String textAlign;
|
||||
|
||||
/**
|
||||
* 行高倍数
|
||||
*/
|
||||
private BigDecimal lineHeight;
|
||||
|
||||
/**
|
||||
* 最大行数
|
||||
*/
|
||||
private Integer maxLines;
|
||||
|
||||
/**
|
||||
* 文本装饰
|
||||
*/
|
||||
private String textDecoration;
|
||||
}
|
||||
146
src/main/java/com/ycwl/basic/puzzle/dto/PuzzleElementDTO.java
Normal file
146
src/main/java/com/ycwl/basic/puzzle/dto/PuzzleElementDTO.java
Normal file
@@ -0,0 +1,146 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 拼图元素DTO
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
public class PuzzleElementDTO {
|
||||
|
||||
/**
|
||||
* 元素ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 元素类型:1-图片 2-文字
|
||||
*/
|
||||
private Integer elementType;
|
||||
|
||||
/**
|
||||
* 元素标识(用于动态数据替换)
|
||||
*/
|
||||
private String elementKey;
|
||||
|
||||
/**
|
||||
* 元素名称
|
||||
*/
|
||||
private String elementName;
|
||||
|
||||
// ===== 位置和布局属性 =====
|
||||
|
||||
/**
|
||||
* X坐标
|
||||
*/
|
||||
private Integer xPosition;
|
||||
|
||||
/**
|
||||
* Y坐标
|
||||
*/
|
||||
private Integer yPosition;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 层级
|
||||
*/
|
||||
private Integer zIndex;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
private Integer rotation;
|
||||
|
||||
/**
|
||||
* 不透明度
|
||||
*/
|
||||
private Integer opacity;
|
||||
|
||||
// ===== 图片元素属性 =====
|
||||
|
||||
/**
|
||||
* 默认图片URL
|
||||
*/
|
||||
private String defaultImageUrl;
|
||||
|
||||
/**
|
||||
* 图片适配模式
|
||||
*/
|
||||
private String imageFitMode;
|
||||
|
||||
/**
|
||||
* 圆角半径
|
||||
*/
|
||||
private Integer borderRadius;
|
||||
|
||||
// ===== 文字元素属性 =====
|
||||
|
||||
/**
|
||||
* 默认文本内容
|
||||
*/
|
||||
private String defaultText;
|
||||
|
||||
/**
|
||||
* 字体
|
||||
*/
|
||||
private String fontFamily;
|
||||
|
||||
/**
|
||||
* 字号
|
||||
*/
|
||||
private Integer fontSize;
|
||||
|
||||
/**
|
||||
* 字体颜色
|
||||
*/
|
||||
private String fontColor;
|
||||
|
||||
/**
|
||||
* 字重
|
||||
*/
|
||||
private String fontWeight;
|
||||
|
||||
/**
|
||||
* 字体样式
|
||||
*/
|
||||
private String fontStyle;
|
||||
|
||||
/**
|
||||
* 对齐方式
|
||||
*/
|
||||
private String textAlign;
|
||||
|
||||
/**
|
||||
* 行高倍数
|
||||
*/
|
||||
private BigDecimal lineHeight;
|
||||
|
||||
/**
|
||||
* 最大行数
|
||||
*/
|
||||
private Integer maxLines;
|
||||
|
||||
/**
|
||||
* 文本装饰
|
||||
*/
|
||||
private String textDecoration;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 拼图生成请求DTO
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
public class PuzzleGenerateRequest {
|
||||
|
||||
/**
|
||||
* 模板编码(必填)
|
||||
*/
|
||||
private String templateCode;
|
||||
|
||||
/**
|
||||
* 用户ID(可选)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 订单ID(可选)
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 业务类型(可选)
|
||||
*/
|
||||
private String businessType;
|
||||
|
||||
/**
|
||||
* 景区ID(可选)
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 动态数据(key为元素的elementKey,value为实际值)
|
||||
* 例如:{"userAvatar": "https://...", "userName": "张三", "orderNumber": "ORDER123"}
|
||||
*/
|
||||
private Map<String, String> dynamicData;
|
||||
|
||||
/**
|
||||
* 输出格式(可选,默认PNG)
|
||||
* 支持:PNG、JPEG
|
||||
*/
|
||||
private String outputFormat;
|
||||
|
||||
/**
|
||||
* 图片质量(可选,默认90,范围0-100)
|
||||
* 仅对JPEG格式有效
|
||||
*/
|
||||
private Integer quality;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 拼图生成响应DTO
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PuzzleGenerateResponse {
|
||||
|
||||
/**
|
||||
* 生成的图片URL
|
||||
*/
|
||||
private String imageUrl;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 图片宽度
|
||||
*/
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 图片高度
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 生成耗时(毫秒)
|
||||
*/
|
||||
private Integer generationDuration;
|
||||
|
||||
/**
|
||||
* 生成记录ID
|
||||
*/
|
||||
private Long recordId;
|
||||
|
||||
/**
|
||||
* 创建成功响应
|
||||
*/
|
||||
public static PuzzleGenerateResponse success(String imageUrl, Long fileSize, Integer width, Integer height, Integer duration, Long recordId) {
|
||||
return new PuzzleGenerateResponse(imageUrl, fileSize, width, height, duration, recordId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图模板详情DTO(包含元素列表)
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
public class PuzzleTemplateDTO {
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 模板编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 画布宽度(像素)
|
||||
*/
|
||||
private Integer canvasWidth;
|
||||
|
||||
/**
|
||||
* 画布高度(像素)
|
||||
*/
|
||||
private Integer canvasHeight;
|
||||
|
||||
/**
|
||||
* 背景类型:0-纯色 1-图片
|
||||
*/
|
||||
private Integer backgroundType;
|
||||
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
private String backgroundColor;
|
||||
|
||||
/**
|
||||
* 背景图片URL
|
||||
*/
|
||||
private String backgroundImage;
|
||||
|
||||
/**
|
||||
* 模板描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 模板分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 元素列表
|
||||
*/
|
||||
private List<PuzzleElementDTO> elements;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ycwl.basic.puzzle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建模板请求DTO
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
public class TemplateCreateRequest {
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 模板编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 画布宽度
|
||||
*/
|
||||
private Integer canvasWidth;
|
||||
|
||||
/**
|
||||
* 画布高度
|
||||
*/
|
||||
private Integer canvasHeight;
|
||||
|
||||
/**
|
||||
* 背景类型:0-纯色 1-图片
|
||||
*/
|
||||
private Integer backgroundType;
|
||||
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
private String backgroundColor;
|
||||
|
||||
/**
|
||||
* 背景图片URL
|
||||
*/
|
||||
private String backgroundImage;
|
||||
|
||||
/**
|
||||
* 模板描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 模板分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.ycwl.basic.puzzle.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 拼图元素实体类
|
||||
* 对应表:puzzle_element
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@TableName("puzzle_element")
|
||||
public class PuzzleElementEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
@TableField("template_id")
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 元素类型:1-图片 2-文字
|
||||
*/
|
||||
@TableField("element_type")
|
||||
private Integer elementType;
|
||||
|
||||
/**
|
||||
* 元素标识(用于动态数据替换)
|
||||
*/
|
||||
@TableField("element_key")
|
||||
private String elementKey;
|
||||
|
||||
/**
|
||||
* 元素名称(便于管理识别)
|
||||
*/
|
||||
@TableField("element_name")
|
||||
private String elementName;
|
||||
|
||||
// ===== 位置和布局属性 =====
|
||||
|
||||
/**
|
||||
* X坐标(相对于画布左上角)
|
||||
*/
|
||||
@TableField("x_position")
|
||||
private Integer xPosition;
|
||||
|
||||
/**
|
||||
* Y坐标(相对于画布左上角)
|
||||
*/
|
||||
@TableField("y_position")
|
||||
private Integer yPosition;
|
||||
|
||||
/**
|
||||
* 宽度(像素)
|
||||
*/
|
||||
@TableField("width")
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 高度(像素)
|
||||
*/
|
||||
@TableField("height")
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 层级(数值越大越靠上)
|
||||
*/
|
||||
@TableField("z_index")
|
||||
private Integer zIndex;
|
||||
|
||||
/**
|
||||
* 旋转角度(0-360度,顺时针)
|
||||
*/
|
||||
@TableField("rotation")
|
||||
private Integer rotation;
|
||||
|
||||
/**
|
||||
* 不透明度(0-100,100为完全不透明)
|
||||
*/
|
||||
@TableField("opacity")
|
||||
private Integer opacity;
|
||||
|
||||
// ===== 图片元素专有属性 =====
|
||||
|
||||
/**
|
||||
* 默认图片URL(图片元素必填)
|
||||
*/
|
||||
@TableField("default_image_url")
|
||||
private String defaultImageUrl;
|
||||
|
||||
/**
|
||||
* 图片适配模式:CONTAIN-等比缩放适应 COVER-等比缩放填充 FILL-拉伸填充 SCALE_DOWN-缩小适应
|
||||
*/
|
||||
@TableField("image_fit_mode")
|
||||
private String imageFitMode;
|
||||
|
||||
/**
|
||||
* 圆角半径(像素,0为直角)
|
||||
*/
|
||||
@TableField("border_radius")
|
||||
private Integer borderRadius;
|
||||
|
||||
// ===== 文字元素专有属性 =====
|
||||
|
||||
/**
|
||||
* 默认文本内容(文字元素必填)
|
||||
*/
|
||||
@TableField("default_text")
|
||||
private String defaultText;
|
||||
|
||||
/**
|
||||
* 字体名称
|
||||
*/
|
||||
@TableField("font_family")
|
||||
private String fontFamily;
|
||||
|
||||
/**
|
||||
* 字号(像素)
|
||||
*/
|
||||
@TableField("font_size")
|
||||
private Integer fontSize;
|
||||
|
||||
/**
|
||||
* 字体颜色(hex格式)
|
||||
*/
|
||||
@TableField("font_color")
|
||||
private String fontColor;
|
||||
|
||||
/**
|
||||
* 字重:NORMAL-正常 BOLD-粗体
|
||||
*/
|
||||
@TableField("font_weight")
|
||||
private String fontWeight;
|
||||
|
||||
/**
|
||||
* 字体样式:NORMAL-正常 ITALIC-斜体
|
||||
*/
|
||||
@TableField("font_style")
|
||||
private String fontStyle;
|
||||
|
||||
/**
|
||||
* 对齐方式:LEFT-左对齐 CENTER-居中 RIGHT-右对齐
|
||||
*/
|
||||
@TableField("text_align")
|
||||
private String textAlign;
|
||||
|
||||
/**
|
||||
* 行高倍数(如:1.5表示1.5倍行距)
|
||||
*/
|
||||
@TableField("line_height")
|
||||
private BigDecimal lineHeight;
|
||||
|
||||
/**
|
||||
* 最大行数(超出后截断,NULL表示不限制)
|
||||
*/
|
||||
@TableField("max_lines")
|
||||
private Integer maxLines;
|
||||
|
||||
/**
|
||||
* 文本装饰:NONE-无 UNDERLINE-下划线 LINE_THROUGH-删除线
|
||||
*/
|
||||
@TableField("text_decoration")
|
||||
private String textDecoration;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 删除标记:0-未删除 1-已删除
|
||||
*/
|
||||
@TableField("deleted")
|
||||
private Integer deleted;
|
||||
|
||||
/**
|
||||
* 删除时间
|
||||
*/
|
||||
@TableField("deleted_at")
|
||||
private Date deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.ycwl.basic.puzzle.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 拼图生成记录实体类
|
||||
* 对应表:puzzle_generation_record
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@TableName("puzzle_generation_record")
|
||||
public class PuzzleGenerationRecordEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
@TableField("template_id")
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 模板编码(冗余字段,方便查询)
|
||||
*/
|
||||
@TableField("template_code")
|
||||
private String templateCode;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 关联订单号
|
||||
*/
|
||||
@TableField("order_id")
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 业务类型(如:order-订单 ticket-门票 certificate-证书)
|
||||
*/
|
||||
@TableField("business_type")
|
||||
private String businessType;
|
||||
|
||||
// ===== 生成参数和结果 =====
|
||||
|
||||
/**
|
||||
* 生成参数(动态数据,JSON格式存储)
|
||||
*/
|
||||
@TableField("generation_params")
|
||||
private String generationParams;
|
||||
|
||||
/**
|
||||
* 生成的图片URL
|
||||
*/
|
||||
@TableField("result_image_url")
|
||||
private String resultImageUrl;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@TableField("result_file_size")
|
||||
private Long resultFileSize;
|
||||
|
||||
/**
|
||||
* 生成图片宽度
|
||||
*/
|
||||
@TableField("result_width")
|
||||
private Integer resultWidth;
|
||||
|
||||
/**
|
||||
* 生成图片高度
|
||||
*/
|
||||
@TableField("result_height")
|
||||
private Integer resultHeight;
|
||||
|
||||
// ===== 状态和性能统计 =====
|
||||
|
||||
/**
|
||||
* 状态:0-生成中 1-成功 2-失败
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 错误信息(失败时记录)
|
||||
*/
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 生成耗时(毫秒)
|
||||
*/
|
||||
@TableField("generation_duration")
|
||||
private Integer generationDuration;
|
||||
|
||||
/**
|
||||
* 重试次数
|
||||
*/
|
||||
@TableField("retry_count")
|
||||
private Integer retryCount;
|
||||
|
||||
// ===== 多租户和扩展字段 =====
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
@TableField("scenic_id")
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 客户端IP
|
||||
*/
|
||||
@TableField("client_ip")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 客户端User-Agent
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.ycwl.basic.puzzle.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 拼图模板实体类
|
||||
* 对应表:puzzle_template
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@TableName("puzzle_template")
|
||||
public class PuzzleTemplateEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 模板编码(用于API调用)
|
||||
*/
|
||||
@TableField("code")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 画布宽度(像素)
|
||||
*/
|
||||
@TableField("canvas_width")
|
||||
private Integer canvasWidth;
|
||||
|
||||
/**
|
||||
* 画布高度(像素)
|
||||
*/
|
||||
@TableField("canvas_height")
|
||||
private Integer canvasHeight;
|
||||
|
||||
/**
|
||||
* 背景类型:0-纯色 1-图片
|
||||
*/
|
||||
@TableField("background_type")
|
||||
private Integer backgroundType;
|
||||
|
||||
/**
|
||||
* 背景颜色(hex格式)
|
||||
*/
|
||||
@TableField("background_color")
|
||||
private String backgroundColor;
|
||||
|
||||
/**
|
||||
* 背景图片URL
|
||||
*/
|
||||
@TableField("background_image")
|
||||
private String backgroundImage;
|
||||
|
||||
/**
|
||||
* 模板描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 模板分类(如:order-订单 ticket-票务 certificate-证书)
|
||||
*/
|
||||
@TableField("category")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 景区ID(用于多租户隔离)
|
||||
*/
|
||||
@TableField("scenic_id")
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 删除标记:0-未删除 1-已删除
|
||||
*/
|
||||
@TableField("deleted")
|
||||
private Integer deleted;
|
||||
|
||||
/**
|
||||
* 删除时间
|
||||
*/
|
||||
@TableField("deleted_at")
|
||||
private Date deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ycwl.basic.puzzle.mapper;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图元素Mapper接口
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public interface PuzzleElementMapper {
|
||||
|
||||
/**
|
||||
* 根据ID查询元素
|
||||
*/
|
||||
PuzzleElementEntity getById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 根据模板ID查询元素列表(按z-index排序)
|
||||
*/
|
||||
List<PuzzleElementEntity> getByTemplateId(@Param("templateId") Long templateId);
|
||||
|
||||
/**
|
||||
* 插入元素
|
||||
*/
|
||||
int insert(PuzzleElementEntity entity);
|
||||
|
||||
/**
|
||||
* 批量插入元素
|
||||
*/
|
||||
int batchInsert(@Param("list") List<PuzzleElementEntity> list);
|
||||
|
||||
/**
|
||||
* 更新元素
|
||||
*/
|
||||
int update(PuzzleElementEntity entity);
|
||||
|
||||
/**
|
||||
* 删除元素(逻辑删除)
|
||||
*/
|
||||
int deleteById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 根据模板ID删除所有元素(逻辑删除)
|
||||
*/
|
||||
int deleteByTemplateId(@Param("templateId") Long templateId);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ycwl.basic.puzzle.mapper;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图生成记录Mapper接口
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public interface PuzzleGenerationRecordMapper {
|
||||
|
||||
/**
|
||||
* 根据ID查询记录
|
||||
*/
|
||||
PuzzleGenerationRecordEntity getById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 查询用户的生成记录列表
|
||||
*/
|
||||
List<PuzzleGenerationRecordEntity> listByUserId(@Param("userId") Long userId,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 查询订单的生成记录列表
|
||||
*/
|
||||
List<PuzzleGenerationRecordEntity> listByOrderId(@Param("orderId") String orderId);
|
||||
|
||||
/**
|
||||
* 插入记录
|
||||
*/
|
||||
int insert(PuzzleGenerationRecordEntity entity);
|
||||
|
||||
/**
|
||||
* 更新记录
|
||||
*/
|
||||
int update(PuzzleGenerationRecordEntity entity);
|
||||
|
||||
/**
|
||||
* 更新为成功状态
|
||||
*/
|
||||
int updateSuccess(@Param("id") Long id,
|
||||
@Param("resultImageUrl") String resultImageUrl,
|
||||
@Param("resultFileSize") Long resultFileSize,
|
||||
@Param("resultWidth") Integer resultWidth,
|
||||
@Param("resultHeight") Integer resultHeight,
|
||||
@Param("generationDuration") Integer generationDuration);
|
||||
|
||||
/**
|
||||
* 更新为失败状态
|
||||
*/
|
||||
int updateFail(@Param("id") Long id,
|
||||
@Param("errorMessage") String errorMessage);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ycwl.basic.puzzle.mapper;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图模板Mapper接口
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public interface PuzzleTemplateMapper {
|
||||
|
||||
/**
|
||||
* 根据ID查询模板
|
||||
*/
|
||||
PuzzleTemplateEntity getById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 根据模板编码查询模板
|
||||
*/
|
||||
PuzzleTemplateEntity getByCode(@Param("code") String code);
|
||||
|
||||
/**
|
||||
* 查询模板列表
|
||||
*/
|
||||
List<PuzzleTemplateEntity> list(@Param("scenicId") Long scenicId,
|
||||
@Param("category") String category,
|
||||
@Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 插入模板
|
||||
*/
|
||||
int insert(PuzzleTemplateEntity entity);
|
||||
|
||||
/**
|
||||
* 更新模板
|
||||
*/
|
||||
int update(PuzzleTemplateEntity entity);
|
||||
|
||||
/**
|
||||
* 删除模板(逻辑删除)
|
||||
*/
|
||||
int deleteById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 检查模板编码是否存在
|
||||
*/
|
||||
int countByCode(@Param("code") String code, @Param("excludeId") Long excludeId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ycwl.basic.puzzle.service;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||
|
||||
/**
|
||||
* 拼图图片生成服务接口
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public interface IPuzzleGenerateService {
|
||||
|
||||
/**
|
||||
* 生成拼图图片
|
||||
*
|
||||
* @param request 生成请求
|
||||
* @return 生成结果(包含图片URL等信息)
|
||||
*/
|
||||
PuzzleGenerateResponse generate(PuzzleGenerateRequest request);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ycwl.basic.puzzle.service;
|
||||
|
||||
import com.ycwl.basic.puzzle.dto.ElementCreateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleElementDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||
import com.ycwl.basic.puzzle.dto.TemplateCreateRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拼图模板管理服务接口
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public interface IPuzzleTemplateService {
|
||||
|
||||
/**
|
||||
* 创建模板
|
||||
*/
|
||||
Long createTemplate(TemplateCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新模板
|
||||
*/
|
||||
void updateTemplate(Long id, TemplateCreateRequest request);
|
||||
|
||||
/**
|
||||
* 删除模板(会同时删除关联的元素)
|
||||
*/
|
||||
void deleteTemplate(Long id);
|
||||
|
||||
/**
|
||||
* 获取模板详情(包含元素列表)
|
||||
*/
|
||||
PuzzleTemplateDTO getTemplateDetail(Long id);
|
||||
|
||||
/**
|
||||
* 根据编码获取模板详情
|
||||
*/
|
||||
PuzzleTemplateDTO getTemplateByCode(String code);
|
||||
|
||||
/**
|
||||
* 获取模板列表
|
||||
*/
|
||||
List<PuzzleTemplateDTO> listTemplates(Long scenicId, String category, Integer status);
|
||||
|
||||
/**
|
||||
* 为模板添加元素
|
||||
*/
|
||||
Long addElement(ElementCreateRequest request);
|
||||
|
||||
/**
|
||||
* 批量添加元素
|
||||
*/
|
||||
void batchAddElements(Long templateId, List<ElementCreateRequest> elements);
|
||||
|
||||
/**
|
||||
* 更新元素
|
||||
*/
|
||||
void updateElement(Long id, ElementCreateRequest request);
|
||||
|
||||
/**
|
||||
* 删除元素
|
||||
*/
|
||||
void deleteElement(Long id);
|
||||
|
||||
/**
|
||||
* 获取元素详情
|
||||
*/
|
||||
PuzzleElementDTO getElementDetail(Long id);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.ycwl.basic.puzzle.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
||||
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 拼图图片生成服务实现
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
|
||||
private final PuzzleTemplateMapper templateMapper;
|
||||
private final PuzzleElementMapper elementMapper;
|
||||
private final PuzzleGenerationRecordMapper recordMapper;
|
||||
private final PuzzleImageRenderer imageRenderer;
|
||||
|
||||
@Override
|
||||
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
log.info("开始生成拼图: templateCode={}, userId={}, orderId={}",
|
||||
request.getTemplateCode(), request.getUserId(), request.getOrderId());
|
||||
|
||||
// 1. 查询模板和元素
|
||||
PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
||||
}
|
||||
|
||||
if (template.getStatus() != 1) {
|
||||
throw new IllegalArgumentException("模板已禁用: " + request.getTemplateCode());
|
||||
}
|
||||
|
||||
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(template.getId());
|
||||
if (elements.isEmpty()) {
|
||||
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||
}
|
||||
|
||||
// 2. 按z-index排序元素
|
||||
elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||
|
||||
// 3. 创建生成记录
|
||||
PuzzleGenerationRecordEntity record = createRecord(template, request);
|
||||
recordMapper.insert(record);
|
||||
|
||||
try {
|
||||
// 4. 渲染图片
|
||||
BufferedImage resultImage = imageRenderer.render(template, elements, request.getDynamicData());
|
||||
|
||||
// 5. 上传到OSS
|
||||
String imageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
|
||||
log.info("图片上传成功: url={}", imageUrl);
|
||||
|
||||
// 6. 更新记录为成功
|
||||
long duration = (int) (System.currentTimeMillis() - startTime);
|
||||
long fileSize = estimateFileSize(resultImage, request.getOutputFormat());
|
||||
recordMapper.updateSuccess(
|
||||
record.getId(),
|
||||
imageUrl,
|
||||
fileSize,
|
||||
resultImage.getWidth(),
|
||||
resultImage.getHeight(),
|
||||
(int) duration
|
||||
);
|
||||
|
||||
log.info("拼图生成成功: recordId={}, imageUrl={}, duration={}ms",
|
||||
record.getId(), imageUrl, duration);
|
||||
|
||||
return PuzzleGenerateResponse.success(
|
||||
imageUrl,
|
||||
fileSize,
|
||||
resultImage.getWidth(),
|
||||
resultImage.getHeight(),
|
||||
(int) duration,
|
||||
record.getId()
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("拼图生成失败: templateCode={}", request.getTemplateCode(), e);
|
||||
// 更新记录为失败
|
||||
recordMapper.updateFail(record.getId(), e.getMessage());
|
||||
throw new RuntimeException("图片生成失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建生成记录
|
||||
*/
|
||||
private PuzzleGenerationRecordEntity createRecord(PuzzleTemplateEntity template, PuzzleGenerateRequest request) {
|
||||
PuzzleGenerationRecordEntity record = new PuzzleGenerationRecordEntity();
|
||||
record.setTemplateId(template.getId());
|
||||
record.setTemplateCode(template.getCode());
|
||||
record.setUserId(request.getUserId());
|
||||
record.setOrderId(request.getOrderId());
|
||||
record.setBusinessType(request.getBusinessType());
|
||||
record.setScenicId(request.getScenicId());
|
||||
record.setStatus(0); // 生成中
|
||||
record.setRetryCount(0);
|
||||
|
||||
// 将动态数据保存为JSON
|
||||
if (request.getDynamicData() != null && !request.getDynamicData().isEmpty()) {
|
||||
record.setGenerationParams(JSONUtil.toJsonStr(request.getDynamicData()));
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片到OSS
|
||||
*/
|
||||
private String uploadImage(BufferedImage image, String templateCode, String format, Integer quality) throws IOException {
|
||||
// 确定格式
|
||||
String outputFormat = StrUtil.isNotBlank(format) ? format.toUpperCase() : "PNG";
|
||||
if (!"PNG".equals(outputFormat) && !"JPEG".equals(outputFormat) && !"JPG".equals(outputFormat)) {
|
||||
outputFormat = "PNG";
|
||||
}
|
||||
|
||||
// 转换为字节数组
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, outputFormat, baos);
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
|
||||
// 生成文件名
|
||||
String fileName = String.format("puzzle/%s/%s.%s",
|
||||
templateCode,
|
||||
UUID.randomUUID().toString().replace("-", ""),
|
||||
outputFormat.toLowerCase()
|
||||
);
|
||||
|
||||
// 使用项目现有的存储工厂上传
|
||||
try {
|
||||
return StorageFactory.use().uploadFile(imageBytes, "puzzle", fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("上传图片失败: fileName={}", fileName, e);
|
||||
throw new IOException("图片上传失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算文件大小(字节)
|
||||
*/
|
||||
private long estimateFileSize(BufferedImage image, String format) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
String outputFormat = StrUtil.isNotBlank(format) ? format.toUpperCase() : "PNG";
|
||||
ImageIO.write(image, outputFormat, baos);
|
||||
return baos.size();
|
||||
} catch (IOException e) {
|
||||
log.warn("估算文件大小失败", e);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.ycwl.basic.puzzle.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.ycwl.basic.puzzle.dto.ElementCreateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleElementDTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||
import com.ycwl.basic.puzzle.dto.TemplateCreateRequest;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 拼图模板管理服务实现
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
|
||||
private final PuzzleTemplateMapper templateMapper;
|
||||
private final PuzzleElementMapper elementMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createTemplate(TemplateCreateRequest request) {
|
||||
log.info("创建拼图模板: code={}, name={}", request.getCode(), request.getName());
|
||||
|
||||
// 检查编码是否已存在
|
||||
int count = templateMapper.countByCode(request.getCode(), null);
|
||||
if (count > 0) {
|
||||
throw new IllegalArgumentException("模板编码已存在: " + request.getCode());
|
||||
}
|
||||
|
||||
// 转换为实体并插入
|
||||
PuzzleTemplateEntity entity = BeanUtil.copyProperties(request, PuzzleTemplateEntity.class);
|
||||
entity.setDeleted(0);
|
||||
templateMapper.insert(entity);
|
||||
|
||||
log.info("拼图模板创建成功: id={}, code={}", entity.getId(), entity.getCode());
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateTemplate(Long id, TemplateCreateRequest request) {
|
||||
log.info("更新拼图模板: id={}", id);
|
||||
|
||||
// 检查模板是否存在
|
||||
PuzzleTemplateEntity existing = templateMapper.getById(id);
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + id);
|
||||
}
|
||||
|
||||
// 如果修改了编码,检查新编码是否已存在
|
||||
if (request.getCode() != null && !request.getCode().equals(existing.getCode())) {
|
||||
int count = templateMapper.countByCode(request.getCode(), id);
|
||||
if (count > 0) {
|
||||
throw new IllegalArgumentException("模板编码已存在: " + request.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新
|
||||
PuzzleTemplateEntity entity = BeanUtil.copyProperties(request, PuzzleTemplateEntity.class);
|
||||
entity.setId(id);
|
||||
templateMapper.update(entity);
|
||||
|
||||
log.info("拼图模板更新成功: id={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteTemplate(Long id) {
|
||||
log.info("删除拼图模板: id={}", id);
|
||||
|
||||
// 检查模板是否存在
|
||||
PuzzleTemplateEntity existing = templateMapper.getById(id);
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + id);
|
||||
}
|
||||
|
||||
// 删除模板及其所有元素
|
||||
templateMapper.deleteById(id);
|
||||
elementMapper.deleteByTemplateId(id);
|
||||
|
||||
log.info("拼图模板删除成功: id={}, 同时删除了关联的元素", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PuzzleTemplateDTO getTemplateDetail(Long id) {
|
||||
log.debug("获取拼图模板详情: id={}", id);
|
||||
|
||||
PuzzleTemplateEntity template = templateMapper.getById(id);
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + id);
|
||||
}
|
||||
|
||||
return convertToDTO(template);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PuzzleTemplateDTO getTemplateByCode(String code) {
|
||||
log.debug("根据编码获取拼图模板: code={}", code);
|
||||
|
||||
PuzzleTemplateEntity template = templateMapper.getByCode(code);
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + code);
|
||||
}
|
||||
|
||||
return convertToDTO(template);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PuzzleTemplateDTO> listTemplates(Long scenicId, String category, Integer status) {
|
||||
log.debug("查询拼图模板列表: scenicId={}, category={}, status={}", scenicId, category, status);
|
||||
|
||||
List<PuzzleTemplateEntity> templates = templateMapper.list(scenicId, category, status);
|
||||
return templates.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long addElement(ElementCreateRequest request) {
|
||||
log.info("添加元素到模板: templateId={}, elementKey={}", request.getTemplateId(), request.getElementKey());
|
||||
|
||||
// 检查模板是否存在
|
||||
PuzzleTemplateEntity template = templateMapper.getById(request.getTemplateId());
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + request.getTemplateId());
|
||||
}
|
||||
|
||||
// 转换为实体并插入
|
||||
PuzzleElementEntity entity = BeanUtil.copyProperties(request, PuzzleElementEntity.class);
|
||||
entity.setDeleted(0);
|
||||
elementMapper.insert(entity);
|
||||
|
||||
log.info("元素添加成功: id={}, elementKey={}", entity.getId(), entity.getElementKey());
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchAddElements(Long templateId, List<ElementCreateRequest> elements) {
|
||||
log.info("批量添加元素到模板: templateId={}, count={}", templateId, elements.size());
|
||||
|
||||
// 检查模板是否存在
|
||||
PuzzleTemplateEntity template = templateMapper.getById(templateId);
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + templateId);
|
||||
}
|
||||
|
||||
// 转换为实体列表
|
||||
List<PuzzleElementEntity> entityList = new ArrayList<>();
|
||||
for (ElementCreateRequest request : elements) {
|
||||
request.setTemplateId(templateId);
|
||||
PuzzleElementEntity entity = BeanUtil.copyProperties(request, PuzzleElementEntity.class);
|
||||
entity.setDeleted(0);
|
||||
entityList.add(entity);
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
if (!entityList.isEmpty()) {
|
||||
elementMapper.batchInsert(entityList);
|
||||
log.info("批量添加元素成功: templateId={}, count={}", templateId, entityList.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateElement(Long id, ElementCreateRequest request) {
|
||||
log.info("更新元素: id={}", id);
|
||||
|
||||
// 检查元素是否存在
|
||||
PuzzleElementEntity existing = elementMapper.getById(id);
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("元素不存在: " + id);
|
||||
}
|
||||
|
||||
// 更新
|
||||
PuzzleElementEntity entity = BeanUtil.copyProperties(request, PuzzleElementEntity.class);
|
||||
entity.setId(id);
|
||||
elementMapper.update(entity);
|
||||
|
||||
log.info("元素更新成功: id={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteElement(Long id) {
|
||||
log.info("删除元素: id={}", id);
|
||||
|
||||
// 检查元素是否存在
|
||||
PuzzleElementEntity existing = elementMapper.getById(id);
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("元素不存在: " + id);
|
||||
}
|
||||
|
||||
elementMapper.deleteById(id);
|
||||
log.info("元素删除成功: id={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PuzzleElementDTO getElementDetail(Long id) {
|
||||
log.debug("获取元素详情: id={}", id);
|
||||
|
||||
PuzzleElementEntity element = elementMapper.getById(id);
|
||||
if (element == null) {
|
||||
throw new IllegalArgumentException("元素不存在: " + id);
|
||||
}
|
||||
|
||||
return BeanUtil.copyProperties(element, PuzzleElementDTO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为DTO(包含元素列表)
|
||||
*/
|
||||
private PuzzleTemplateDTO convertToDTO(PuzzleTemplateEntity template) {
|
||||
PuzzleTemplateDTO dto = BeanUtil.copyProperties(template, PuzzleTemplateDTO.class);
|
||||
|
||||
// 查询元素列表
|
||||
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(template.getId());
|
||||
List<PuzzleElementDTO> elementDTOs = elements.stream()
|
||||
.map(e -> BeanUtil.copyProperties(e, PuzzleElementDTO.class))
|
||||
.collect(Collectors.toList());
|
||||
dto.setElements(elementDTOs);
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package com.ycwl.basic.puzzle.util;
|
||||
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 拼图图片渲染引擎
|
||||
* 核心功能:将模板和元素渲染成最终图片
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PuzzleImageRenderer {
|
||||
|
||||
/**
|
||||
* 渲染拼图图片
|
||||
*
|
||||
* @param template 模板配置
|
||||
* @param elements 元素列表(已按z-index排序)
|
||||
* @param dynamicData 动态数据(key=elementKey, value=实际值)
|
||||
* @return 渲染后的图片
|
||||
*/
|
||||
public BufferedImage render(PuzzleTemplateEntity template,
|
||||
List<PuzzleElementEntity> elements,
|
||||
Map<String, String> dynamicData) {
|
||||
log.info("开始渲染拼图: templateId={}, elementCount={}", template.getId(), elements.size());
|
||||
|
||||
// 1. 创建画布
|
||||
BufferedImage canvas = new BufferedImage(
|
||||
template.getCanvasWidth(),
|
||||
template.getCanvasHeight(),
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
|
||||
Graphics2D g2d = canvas.createGraphics();
|
||||
|
||||
try {
|
||||
// 2. 开启抗锯齿和优化渲染质量
|
||||
enableHighQualityRendering(g2d);
|
||||
|
||||
// 3. 绘制背景
|
||||
drawBackground(g2d, template);
|
||||
|
||||
// 4. 按z-index顺序绘制元素
|
||||
for (PuzzleElementEntity element : elements) {
|
||||
try {
|
||||
if (element.getElementType() == 1) {
|
||||
// 图片元素
|
||||
drawImageElement(g2d, element, dynamicData);
|
||||
} else if (element.getElementType() == 2) {
|
||||
// 文字元素
|
||||
drawTextElement(g2d, element, dynamicData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("绘制元素失败: elementId={}, elementKey={}", element.getId(), element.getElementKey(), e);
|
||||
// 继续绘制其他元素,不中断整个渲染流程
|
||||
}
|
||||
}
|
||||
|
||||
log.info("拼图渲染完成: templateId={}", template.getId());
|
||||
return canvas;
|
||||
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启高质量渲染
|
||||
*/
|
||||
private 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制背景
|
||||
*/
|
||||
private void drawBackground(Graphics2D g2d, PuzzleTemplateEntity template) {
|
||||
if (template.getBackgroundType() == 0) {
|
||||
// 纯色背景
|
||||
String bgColor = StrUtil.isNotBlank(template.getBackgroundColor())
|
||||
? template.getBackgroundColor() : "#FFFFFF";
|
||||
g2d.setColor(parseColor(bgColor));
|
||||
g2d.fillRect(0, 0, template.getCanvasWidth(), template.getCanvasHeight());
|
||||
} else if (template.getBackgroundType() == 1 && StrUtil.isNotBlank(template.getBackgroundImage())) {
|
||||
// 图片背景
|
||||
try {
|
||||
BufferedImage bgImage = downloadImage(template.getBackgroundImage());
|
||||
BufferedImage scaledBg = ImgUtil.scale(bgImage, template.getCanvasWidth(), template.getCanvasHeight());
|
||||
g2d.drawImage(scaledBg, 0, 0, null);
|
||||
} catch (Exception e) {
|
||||
log.error("绘制背景图片失败: {}", template.getBackgroundImage(), e);
|
||||
// 降级为白色背景
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillRect(0, 0, template.getCanvasWidth(), template.getCanvasHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制图片元素
|
||||
*/
|
||||
private void drawImageElement(Graphics2D g2d, PuzzleElementEntity element, Map<String, String> dynamicData) {
|
||||
// 获取图片URL(优先使用动态数据)
|
||||
String imageUrl = dynamicData.getOrDefault(element.getElementKey(), element.getDefaultImageUrl());
|
||||
if (StrUtil.isBlank(imageUrl)) {
|
||||
log.warn("图片元素没有图片URL: elementKey={}", element.getElementKey());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 下载图片
|
||||
BufferedImage image = downloadImage(imageUrl);
|
||||
|
||||
// 应用透明度
|
||||
float opacity = (element.getOpacity() != null ? element.getOpacity() : 100) / 100f;
|
||||
Composite originalComposite = g2d.getComposite();
|
||||
if (opacity < 1.0f) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
|
||||
}
|
||||
|
||||
// 缩放图片
|
||||
BufferedImage scaledImage = scaleImage(image, element);
|
||||
|
||||
// 绘制(支持圆角)
|
||||
Integer borderRadius = element.getBorderRadius() != null ? element.getBorderRadius() : 0;
|
||||
if (borderRadius > 0) {
|
||||
drawRoundedImage(g2d, scaledImage, element.getXPosition(), element.getYPosition(),
|
||||
element.getWidth(), element.getHeight(), borderRadius);
|
||||
} else {
|
||||
g2d.drawImage(scaledImage, element.getXPosition(), element.getYPosition(),
|
||||
element.getWidth(), element.getHeight(), null);
|
||||
}
|
||||
|
||||
// 恢复透明度
|
||||
g2d.setComposite(originalComposite);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("绘制图片元素失败: elementKey={}, imageUrl={}", element.getElementKey(), imageUrl, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放图片
|
||||
*/
|
||||
private BufferedImage scaleImage(BufferedImage source, PuzzleElementEntity element) {
|
||||
String fitMode = StrUtil.isNotBlank(element.getImageFitMode()) ? element.getImageFitMode() : "CONTAIN";
|
||||
|
||||
switch (fitMode) {
|
||||
case "COVER":
|
||||
// 等比缩放填充(可能裁剪)
|
||||
return ImgUtil.scale(source, element.getWidth(), element.getHeight(), Color.TRANSPARENT);
|
||||
case "FILL":
|
||||
// 拉伸填充
|
||||
return ImgUtil.scale(source, element.getWidth(), element.getHeight());
|
||||
case "SCALE_DOWN":
|
||||
// 缩小适应(不放大)
|
||||
if (source.getWidth() <= element.getWidth() && source.getHeight() <= element.getHeight()) {
|
||||
return source;
|
||||
}
|
||||
return ImgUtil.scale(source, element.getWidth(), element.getHeight(), Color.TRANSPARENT);
|
||||
case "CONTAIN":
|
||||
default:
|
||||
// 等比缩放适应
|
||||
return ImgUtil.scale(source, element.getWidth(), element.getHeight(), Color.TRANSPARENT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制圆角图片
|
||||
*/
|
||||
private void drawRoundedImage(Graphics2D g2d, BufferedImage image, int x, int y, int width, int height, int radius) {
|
||||
// 创建圆角遮罩
|
||||
BufferedImage rounded = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = rounded.createGraphics();
|
||||
enableHighQualityRendering(g);
|
||||
|
||||
// 绘制圆角矩形
|
||||
g.setColor(Color.WHITE);
|
||||
g.fill(new RoundRectangle2D.Float(0, 0, width, height, radius * 2, radius * 2));
|
||||
|
||||
// 应用遮罩
|
||||
g.setComposite(AlphaComposite.SrcAtop);
|
||||
g.drawImage(image, 0, 0, width, height, null);
|
||||
g.dispose();
|
||||
|
||||
// 绘制到画布
|
||||
g2d.drawImage(rounded, x, y, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制文字元素
|
||||
*/
|
||||
private void drawTextElement(Graphics2D g2d, PuzzleElementEntity element, Map<String, String> dynamicData) {
|
||||
// 获取文本内容(优先使用动态数据)
|
||||
String text = dynamicData.getOrDefault(element.getElementKey(), element.getDefaultText());
|
||||
if (StrUtil.isBlank(text)) {
|
||||
log.debug("文字元素没有文本内容: elementKey={}", element.getElementKey());
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置字体
|
||||
int fontStyle = getFontStyle(element);
|
||||
String fontFamily = StrUtil.isNotBlank(element.getFontFamily()) ? element.getFontFamily() : "微软雅黑";
|
||||
int fontSize = element.getFontSize() != null ? element.getFontSize() : 14;
|
||||
Font font = new Font(fontFamily, fontStyle, fontSize);
|
||||
g2d.setFont(font);
|
||||
|
||||
// 设置颜色
|
||||
String fontColor = StrUtil.isNotBlank(element.getFontColor()) ? element.getFontColor() : "#000000";
|
||||
g2d.setColor(parseColor(fontColor));
|
||||
|
||||
// 设置透明度
|
||||
float opacity = (element.getOpacity() != null ? element.getOpacity() : 100) / 100f;
|
||||
Composite originalComposite = g2d.getComposite();
|
||||
if (opacity < 1.0f) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
|
||||
}
|
||||
|
||||
// 绘制文本
|
||||
drawText(g2d, text, element);
|
||||
|
||||
// 恢复透明度
|
||||
g2d.setComposite(originalComposite);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制文本(支持对齐、行高、最大行数)
|
||||
*/
|
||||
private void drawText(Graphics2D g2d, String text, PuzzleElementEntity element) {
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int lineHeight = (int) (fm.getHeight() * (element.getLineHeight() != null ? element.getLineHeight().floatValue() : 1.5f));
|
||||
|
||||
// 简单处理:绘制单行或多行文本
|
||||
String[] lines = text.split("\n");
|
||||
Integer maxLines = element.getMaxLines();
|
||||
int actualLines = maxLines != null ? Math.min(lines.length, maxLines) : lines.length;
|
||||
|
||||
String textAlign = StrUtil.isNotBlank(element.getTextAlign()) ? element.getTextAlign() : "LEFT";
|
||||
|
||||
int y = element.getYPosition() + fm.getAscent();
|
||||
for (int i = 0; i < actualLines; i++) {
|
||||
String line = lines[i];
|
||||
int x = calculateTextX(line, element, fm, textAlign);
|
||||
g2d.drawString(line, x, y);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文本X坐标(根据对齐方式)
|
||||
*/
|
||||
private int calculateTextX(String text, PuzzleElementEntity element, FontMetrics fm, String align) {
|
||||
int textWidth = fm.stringWidth(text);
|
||||
switch (align) {
|
||||
case "CENTER":
|
||||
return element.getXPosition() + (element.getWidth() - textWidth) / 2;
|
||||
case "RIGHT":
|
||||
return element.getXPosition() + element.getWidth() - textWidth;
|
||||
case "LEFT":
|
||||
default:
|
||||
return element.getXPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字体样式
|
||||
*/
|
||||
private int getFontStyle(PuzzleElementEntity element) {
|
||||
int style = Font.PLAIN;
|
||||
String fontWeight = StrUtil.isNotBlank(element.getFontWeight()) ? element.getFontWeight() : "NORMAL";
|
||||
String fontStyleStr = StrUtil.isNotBlank(element.getFontStyle()) ? element.getFontStyle() : "NORMAL";
|
||||
|
||||
if ("BOLD".equalsIgnoreCase(fontWeight)) {
|
||||
style |= Font.BOLD;
|
||||
}
|
||||
if ("ITALIC".equalsIgnoreCase(fontStyleStr)) {
|
||||
style |= Font.ITALIC;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片
|
||||
*/
|
||||
private BufferedImage downloadImage(String imageUrl) throws IOException {
|
||||
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
|
||||
// 网络图片
|
||||
byte[] imageBytes = HttpUtil.downloadBytes(imageUrl);
|
||||
return ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
} else {
|
||||
// 本地文件
|
||||
return ImageIO.read(new File(imageUrl));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析颜色
|
||||
*/
|
||||
private Color parseColor(String colorStr) {
|
||||
try {
|
||||
if (colorStr.startsWith("#")) {
|
||||
return Color.decode(colorStr);
|
||||
} else if (colorStr.startsWith("rgb(")) {
|
||||
// 简单解析 rgb(r,g,b)
|
||||
String rgb = colorStr.substring(4, colorStr.length() - 1);
|
||||
String[] parts = rgb.split(",");
|
||||
return new Color(
|
||||
Integer.parseInt(parts[0].trim()),
|
||||
Integer.parseInt(parts[1].trim()),
|
||||
Integer.parseInt(parts[2].trim())
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析颜色失败: {}, 使用黑色", colorStr);
|
||||
}
|
||||
return Color.BLACK;
|
||||
}
|
||||
}
|
||||
153
src/main/resources/mapper/PuzzleElementMapper.xml
Normal file
153
src/main/resources/mapper/PuzzleElementMapper.xml
Normal file
@@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.puzzle.mapper.PuzzleElementMapper">
|
||||
|
||||
<!-- 结果映射 -->
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.puzzle.entity.PuzzleElementEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="template_id" property="templateId"/>
|
||||
<result column="element_type" property="elementType"/>
|
||||
<result column="element_key" property="elementKey"/>
|
||||
<result column="element_name" property="elementName"/>
|
||||
<result column="x_position" property="xPosition"/>
|
||||
<result column="y_position" property="yPosition"/>
|
||||
<result column="width" property="width"/>
|
||||
<result column="height" property="height"/>
|
||||
<result column="z_index" property="zIndex"/>
|
||||
<result column="rotation" property="rotation"/>
|
||||
<result column="opacity" property="opacity"/>
|
||||
<result column="default_image_url" property="defaultImageUrl"/>
|
||||
<result column="image_fit_mode" property="imageFitMode"/>
|
||||
<result column="border_radius" property="borderRadius"/>
|
||||
<result column="default_text" property="defaultText"/>
|
||||
<result column="font_family" property="fontFamily"/>
|
||||
<result column="font_size" property="fontSize"/>
|
||||
<result column="font_color" property="fontColor"/>
|
||||
<result column="font_weight" property="fontWeight"/>
|
||||
<result column="font_style" property="fontStyle"/>
|
||||
<result column="text_align" property="textAlign"/>
|
||||
<result column="line_height" property="lineHeight"/>
|
||||
<result column="max_lines" property="maxLines"/>
|
||||
<result column="text_decoration" property="textDecoration"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="deleted" property="deleted"/>
|
||||
<result column="deleted_at" property="deletedAt"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 基础列 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, template_id, element_type, element_key, element_name,
|
||||
x_position, y_position, width, height, z_index, rotation, opacity,
|
||||
default_image_url, image_fit_mode, border_radius,
|
||||
default_text, font_family, font_size, font_color, font_weight, font_style,
|
||||
text_align, line_height, max_lines, text_decoration,
|
||||
create_time, update_time, deleted, deleted_at
|
||||
</sql>
|
||||
|
||||
<!-- 根据ID查询 -->
|
||||
<select id="getById" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_element
|
||||
WHERE id = #{id} AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 根据模板ID查询元素列表(按z-index排序) -->
|
||||
<select id="getByTemplateId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_element
|
||||
WHERE template_id = #{templateId} AND deleted = 0
|
||||
ORDER BY z_index ASC, id ASC
|
||||
</select>
|
||||
|
||||
<!-- 插入 -->
|
||||
<insert id="insert" parameterType="com.ycwl.basic.puzzle.entity.PuzzleElementEntity"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO puzzle_element (
|
||||
template_id, element_type, element_key, element_name,
|
||||
x_position, y_position, width, height, z_index, rotation, opacity,
|
||||
default_image_url, image_fit_mode, border_radius,
|
||||
default_text, font_family, font_size, font_color, font_weight, font_style,
|
||||
text_align, line_height, max_lines, text_decoration,
|
||||
create_time, update_time, deleted
|
||||
) VALUES (
|
||||
#{templateId}, #{elementType}, #{elementKey}, #{elementName},
|
||||
#{xPosition}, #{yPosition}, #{width}, #{height}, #{zIndex}, #{rotation}, #{opacity},
|
||||
#{defaultImageUrl}, #{imageFitMode}, #{borderRadius},
|
||||
#{defaultText}, #{fontFamily}, #{fontSize}, #{fontColor}, #{fontWeight}, #{fontStyle},
|
||||
#{textAlign}, #{lineHeight}, #{maxLines}, #{textDecoration},
|
||||
NOW(), NOW(), 0
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 批量插入 -->
|
||||
<insert id="batchInsert">
|
||||
INSERT INTO puzzle_element (
|
||||
template_id, element_type, element_key, element_name,
|
||||
x_position, y_position, width, height, z_index, rotation, opacity,
|
||||
default_image_url, image_fit_mode, border_radius,
|
||||
default_text, font_family, font_size, font_color, font_weight, font_style,
|
||||
text_align, line_height, max_lines, text_decoration,
|
||||
create_time, update_time, deleted
|
||||
) VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(
|
||||
#{item.templateId}, #{item.elementType}, #{item.elementKey}, #{item.elementName},
|
||||
#{item.xPosition}, #{item.yPosition}, #{item.width}, #{item.height}, #{item.zIndex}, #{item.rotation}, #{item.opacity},
|
||||
#{item.defaultImageUrl}, #{item.imageFitMode}, #{item.borderRadius},
|
||||
#{item.defaultText}, #{item.fontFamily}, #{item.fontSize}, #{item.fontColor}, #{item.fontWeight}, #{item.fontStyle},
|
||||
#{item.textAlign}, #{item.lineHeight}, #{item.maxLines}, #{item.textDecoration},
|
||||
NOW(), NOW(), 0
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 更新 -->
|
||||
<update id="update" parameterType="com.ycwl.basic.puzzle.entity.PuzzleElementEntity">
|
||||
UPDATE puzzle_element
|
||||
<set>
|
||||
<if test="elementType != null">element_type = #{elementType},</if>
|
||||
<if test="elementKey != null">element_key = #{elementKey},</if>
|
||||
<if test="elementName != null">element_name = #{elementName},</if>
|
||||
<if test="xPosition != null">x_position = #{xPosition},</if>
|
||||
<if test="yPosition != null">y_position = #{yPosition},</if>
|
||||
<if test="width != null">width = #{width},</if>
|
||||
<if test="height != null">height = #{height},</if>
|
||||
<if test="zIndex != null">z_index = #{zIndex},</if>
|
||||
<if test="rotation != null">rotation = #{rotation},</if>
|
||||
<if test="opacity != null">opacity = #{opacity},</if>
|
||||
<if test="defaultImageUrl != null">default_image_url = #{defaultImageUrl},</if>
|
||||
<if test="imageFitMode != null">image_fit_mode = #{imageFitMode},</if>
|
||||
<if test="borderRadius != null">border_radius = #{borderRadius},</if>
|
||||
<if test="defaultText != null">default_text = #{defaultText},</if>
|
||||
<if test="fontFamily != null">font_family = #{fontFamily},</if>
|
||||
<if test="fontSize != null">font_size = #{fontSize},</if>
|
||||
<if test="fontColor != null">font_color = #{fontColor},</if>
|
||||
<if test="fontWeight != null">font_weight = #{fontWeight},</if>
|
||||
<if test="fontStyle != null">font_style = #{fontStyle},</if>
|
||||
<if test="textAlign != null">text_align = #{textAlign},</if>
|
||||
<if test="lineHeight != null">line_height = #{lineHeight},</if>
|
||||
<if test="maxLines != null">max_lines = #{maxLines},</if>
|
||||
<if test="textDecoration != null">text_decoration = #{textDecoration},</if>
|
||||
update_time = NOW()
|
||||
</set>
|
||||
WHERE id = #{id} AND deleted = 0
|
||||
</update>
|
||||
|
||||
<!-- 逻辑删除 -->
|
||||
<update id="deleteById">
|
||||
UPDATE puzzle_element
|
||||
SET deleted = 1, deleted_at = NOW(), update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- 根据模板ID删除所有元素 -->
|
||||
<update id="deleteByTemplateId">
|
||||
UPDATE puzzle_element
|
||||
SET deleted = 1, deleted_at = NOW(), update_time = NOW()
|
||||
WHERE template_id = #{templateId}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
120
src/main/resources/mapper/PuzzleGenerationRecordMapper.xml
Normal file
120
src/main/resources/mapper/PuzzleGenerationRecordMapper.xml
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper">
|
||||
|
||||
<!-- 结果映射 -->
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="template_id" property="templateId"/>
|
||||
<result column="template_code" property="templateCode"/>
|
||||
<result column="user_id" property="userId"/>
|
||||
<result column="order_id" property="orderId"/>
|
||||
<result column="business_type" property="businessType"/>
|
||||
<result column="generation_params" property="generationParams"/>
|
||||
<result column="result_image_url" property="resultImageUrl"/>
|
||||
<result column="result_file_size" property="resultFileSize"/>
|
||||
<result column="result_width" property="resultWidth"/>
|
||||
<result column="result_height" property="resultHeight"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="error_message" property="errorMessage"/>
|
||||
<result column="generation_duration" property="generationDuration"/>
|
||||
<result column="retry_count" property="retryCount"/>
|
||||
<result column="scenic_id" property="scenicId"/>
|
||||
<result column="client_ip" property="clientIp"/>
|
||||
<result column="user_agent" property="userAgent"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 基础列 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, template_id, template_code, user_id, order_id, business_type,
|
||||
generation_params, result_image_url, result_file_size, result_width, result_height,
|
||||
status, error_message, generation_duration, retry_count,
|
||||
scenic_id, client_ip, user_agent, create_time, update_time
|
||||
</sql>
|
||||
|
||||
<!-- 根据ID查询 -->
|
||||
<select id="getById" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_generation_record
|
||||
WHERE id = #{id}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 查询用户的生成记录列表 -->
|
||||
<select id="listByUserId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_generation_record
|
||||
WHERE user_id = #{userId}
|
||||
ORDER BY create_time DESC
|
||||
<if test="limit != null">
|
||||
LIMIT #{limit}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 查询订单的生成记录列表 -->
|
||||
<select id="listByOrderId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_generation_record
|
||||
WHERE order_id = #{orderId}
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 插入 -->
|
||||
<insert id="insert" parameterType="com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO puzzle_generation_record (
|
||||
template_id, template_code, user_id, order_id, business_type,
|
||||
generation_params, result_image_url, result_file_size, result_width, result_height,
|
||||
status, error_message, generation_duration, retry_count,
|
||||
scenic_id, client_ip, user_agent, create_time, update_time
|
||||
) VALUES (
|
||||
#{templateId}, #{templateCode}, #{userId}, #{orderId}, #{businessType},
|
||||
#{generationParams}, #{resultImageUrl}, #{resultFileSize}, #{resultWidth}, #{resultHeight},
|
||||
#{status}, #{errorMessage}, #{generationDuration}, #{retryCount},
|
||||
#{scenicId}, #{clientIp}, #{userAgent}, NOW(), NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 更新 -->
|
||||
<update id="update" parameterType="com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity">
|
||||
UPDATE puzzle_generation_record
|
||||
<set>
|
||||
<if test="resultImageUrl != null">result_image_url = #{resultImageUrl},</if>
|
||||
<if test="resultFileSize != null">result_file_size = #{resultFileSize},</if>
|
||||
<if test="resultWidth != null">result_width = #{resultWidth},</if>
|
||||
<if test="resultHeight != null">result_height = #{resultHeight},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="errorMessage != null">error_message = #{errorMessage},</if>
|
||||
<if test="generationDuration != null">generation_duration = #{generationDuration},</if>
|
||||
<if test="retryCount != null">retry_count = #{retryCount},</if>
|
||||
update_time = NOW()
|
||||
</set>
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- 更新为成功状态 -->
|
||||
<update id="updateSuccess">
|
||||
UPDATE puzzle_generation_record
|
||||
SET status = 1,
|
||||
result_image_url = #{resultImageUrl},
|
||||
result_file_size = #{resultFileSize},
|
||||
result_width = #{resultWidth},
|
||||
result_height = #{resultHeight},
|
||||
generation_duration = #{generationDuration},
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- 更新为失败状态 -->
|
||||
<update id="updateFail">
|
||||
UPDATE puzzle_generation_record
|
||||
SET status = 2,
|
||||
error_message = #{errorMessage},
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
114
src/main/resources/mapper/PuzzleTemplateMapper.xml
Normal file
114
src/main/resources/mapper/PuzzleTemplateMapper.xml
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper">
|
||||
|
||||
<!-- 结果映射 -->
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="name" property="name"/>
|
||||
<result column="code" property="code"/>
|
||||
<result column="canvas_width" property="canvasWidth"/>
|
||||
<result column="canvas_height" property="canvasHeight"/>
|
||||
<result column="background_type" property="backgroundType"/>
|
||||
<result column="background_color" property="backgroundColor"/>
|
||||
<result column="background_image" property="backgroundImage"/>
|
||||
<result column="description" property="description"/>
|
||||
<result column="category" property="category"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="scenic_id" property="scenicId"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="deleted" property="deleted"/>
|
||||
<result column="deleted_at" property="deletedAt"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 基础列 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, name, code, canvas_width, canvas_height, background_type, background_color,
|
||||
background_image, description, category, status, scenic_id, create_time, update_time, deleted, deleted_at
|
||||
</sql>
|
||||
|
||||
<!-- 根据ID查询 -->
|
||||
<select id="getById" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_template
|
||||
WHERE id = #{id} AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 根据编码查询 -->
|
||||
<select id="getByCode" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_template
|
||||
WHERE code = #{code} AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 查询列表 -->
|
||||
<select id="list" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_template
|
||||
WHERE deleted = 0
|
||||
<if test="scenicId != null">
|
||||
AND (scenic_id = #{scenicId} OR scenic_id IS NULL)
|
||||
</if>
|
||||
<if test="category != null and category != ''">
|
||||
AND category = #{category}
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 插入 -->
|
||||
<insert id="insert" parameterType="com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO puzzle_template (
|
||||
name, code, canvas_width, canvas_height, background_type, background_color,
|
||||
background_image, description, category, status, scenic_id, create_time, update_time, deleted
|
||||
) VALUES (
|
||||
#{name}, #{code}, #{canvasWidth}, #{canvasHeight}, #{backgroundType}, #{backgroundColor},
|
||||
#{backgroundImage}, #{description}, #{category}, #{status}, #{scenicId}, NOW(), NOW(), 0
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 更新 -->
|
||||
<update id="update" parameterType="com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity">
|
||||
UPDATE puzzle_template
|
||||
<set>
|
||||
<if test="name != null">name = #{name},</if>
|
||||
<if test="code != null">code = #{code},</if>
|
||||
<if test="canvasWidth != null">canvas_width = #{canvasWidth},</if>
|
||||
<if test="canvasHeight != null">canvas_height = #{canvasHeight},</if>
|
||||
<if test="backgroundType != null">background_type = #{backgroundType},</if>
|
||||
<if test="backgroundColor != null">background_color = #{backgroundColor},</if>
|
||||
<if test="backgroundImage != null">background_image = #{backgroundImage},</if>
|
||||
<if test="description != null">description = #{description},</if>
|
||||
<if test="category != null">category = #{category},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="scenicId != null">scenic_id = #{scenicId},</if>
|
||||
update_time = NOW()
|
||||
</set>
|
||||
WHERE id = #{id} AND deleted = 0
|
||||
</update>
|
||||
|
||||
<!-- 逻辑删除 -->
|
||||
<update id="deleteById">
|
||||
UPDATE puzzle_template
|
||||
SET deleted = 1, deleted_at = NOW(), update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- 检查编码是否存在 -->
|
||||
<select id="countByCode" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM puzzle_template
|
||||
WHERE code = #{code} AND deleted = 0
|
||||
<if test="excludeId != null">
|
||||
AND id != #{excludeId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user