feat(puzzle): 实现拼图生成功能模块

- 新增拼图生成控制器 PuzzleGenerateController,支持 /api/puzzle/generate 接口
- 新增拼图模板管理控制器 PuzzleTemplateController,提供完整的 CRUD 和元素管理功能
- 定义拼图相关 DTO 类,包括模板、元素、生成请求与响应等数据传输对象
- 创建拼图相关的实体类 PuzzleTemplateEntity、PuzzleElementEntity 和 PuzzleGenerationRecordEntity
- 实现 Mapper 接口用于数据库操作,支持模板和元素的增删改查及生成记录管理
- 开发 PuzzleGenerateServiceImpl 服务,完成从模板渲染到图片上传的完整流程
- 提供 PuzzleTemplateServiceImpl 服务,实现模板及其元素的全生命周期管理
- 引入 PuzzleImageRenderer 工具类负责图像合成渲染逻辑
- 支持将生成结果上传至 OSS 并记录生成过程的日志和元数据
This commit is contained in:
2025-11-17 12:54:56 +08:00
parent 630d344b5a
commit 443f92ff92
22 changed files with 2600 additions and 0 deletions

View 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>

View 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>

View 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>