From 2ba868b3b68599387dd1e5a1a1f32912f67f0a3c Mon Sep 17 00:00:00 2001 From: Timi Date: Thu, 6 Nov 2025 14:47:48 +0800 Subject: [PATCH] support add media thumb attachment --- .../api/modules/common/bean/MediaAttach.java | 39 +++++ .../api/modules/common/entity/Attachment.java | 28 +++- .../common/mapper/AttachmentMapper.java | 8 +- .../common/service/AttachmentService.java | 41 +++++- .../implement/AttachmentServiceImplement.java | 139 +++++++++++++++--- src/main/java/com/imyeyu/api/util/JavaCV.java | 42 ++++++ .../mapper/common/AttachmentMapper.xml | 61 +++++++- 7 files changed, 318 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/imyeyu/api/modules/common/bean/MediaAttach.java create mode 100644 src/main/java/com/imyeyu/api/util/JavaCV.java diff --git a/src/main/java/com/imyeyu/api/modules/common/bean/MediaAttach.java b/src/main/java/com/imyeyu/api/modules/common/bean/MediaAttach.java new file mode 100644 index 0000000..177a3da --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/bean/MediaAttach.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.common.bean; + +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-10-20 15:04 + */ +public class MediaAttach { + + /** + * @author 夜雨 + * @since 2025-09-28 02:01 + */ + public enum Type { + + SOURCE, + + THUMB + } + + /** + * + * + * @author 夜雨 + * @since 2025-10-20 15:04 + */ + @Data + public static class ExtData { + + private Long sourceId; + + private String sourceMongoId; + + private boolean isImage; + + private boolean isVideo; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java index 650053e..859a5f0 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java @@ -1,7 +1,7 @@ package com.imyeyu.api.modules.common.entity; -import com.imyeyu.java.ref.Ref; import com.imyeyu.api.bean.MultilingualHandler; +import com.imyeyu.java.ref.Ref; import com.imyeyu.spring.entity.Entity; import lombok.AllArgsConstructor; import lombok.Data; @@ -44,24 +44,36 @@ public class Attachment extends Entity implements MultilingualHandler { /** 镜像 */ MIRROR, + JOURNAL, + + JOURNAL_TRAVEL, + + JOURNAL_MOMENT, + /** 系统 */ SYSTEM } - private BizType bizType; + protected BizType bizType; - private Long bizId; + protected Long bizId; - private String attachType; + protected String attachType; - private String mongoId; + protected String mongoId; @MultilingualField - private String title; + protected String title; - private String name; + protected String name; - private Long size; + protected Long size; + + protected String md5; + + protected String ext; + + protected Long destroyAt; public void setAttachTypeValue(Enum attachType) { this.attachType = attachType.toString(); diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java index 8057e19..acba67c 100644 --- a/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java @@ -1,6 +1,7 @@ package com.imyeyu.api.modules.common.mapper; import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Select; @@ -24,8 +25,9 @@ public interface AttachmentMapper extends BaseMapper { @Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND attach_type = #{attachType} " + VALID + LIMIT_1) Attachment selectByAttachType(Attachment.BizType bizType, long bizId, Enum attachType); - @Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND " + VALID + PAGE) - List listByBizId(Attachment.BizType bizType, long bizId, long offset, int limit); + List listByBizId(Attachment.BizType bizType, Long bizId, List> attachTypes, Page page); - List listByAttachType(Attachment.BizType bizType, long bizId, Enum ...attachTypes); + long countByBizId(Attachment.BizType bizType, Long bizId, List> attachTypes); + + List listByMd5s(Attachment.BizType bizType, Long bizId, List> attachTypes, List md5s); } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java b/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java index d6d60c2..44869e4 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java @@ -1,12 +1,16 @@ package com.imyeyu.api.modules.common.service; -import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest; import com.imyeyu.api.modules.common.vo.attachment.AttachmentView; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.DeletableService; import com.imyeyu.spring.service.DestroyableService; import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.PageableService; +import com.imyeyu.spring.service.UpdatableService; import com.mongodb.client.gridfs.model.GridFSFile; import java.io.InputStream; @@ -20,7 +24,7 @@ import java.util.List; * @author 夜雨 * @since 2023-08-15 10:21 */ -public interface AttachmentService extends GettableService, DeletableService, DestroyableService { +public interface AttachmentService extends GettableService, PageableService, UpdatableService, DeletableService, DestroyableService { /** * @@ -29,6 +33,16 @@ public interface AttachmentService extends GettableService, De */ void create(AttachmentRequest request); + /** + * 创建媒体附件,同步创建缩略图 + * + * @param request 附件请求 + * @return 缩略图附件 + */ + Attachment createMedia(AttachmentRequest request) throws TimiException; + + void deleteMedia(Long thumbId) throws TimiException; + Attachment getByBizId(Attachment.BizType bizType, long bizId); Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum attachType); @@ -46,11 +60,26 @@ public interface AttachmentService extends GettableService, De /** * 根据业务获取所有附件 * - * @param bizType 业务类型 - * @param bizId 业务 ID - * @param attachTypes + * @param bizType 业务类型 + * @param bizId 业务 ID,可为 null + * @param attachTypes 附件类型,可为 null * @return 所有附件 * @throws TimiException 服务异常 */ - List listByBizId(Attachment.BizType bizType, long bizId, Enum ...attachTypes); + List listByBizId(Attachment.BizType bizType, Long bizId, Enum ...attachTypes); + + /** + * 根据业务获取所有附件 + * + * @param bizType 业务类型 + * @param bizId 业务 ID,可为 null + * @param attachTypes 附件类型,可为 null + * @return 附件数量 + * @throws TimiException 服务异常 + */ + long countByBizId(Attachment.BizType bizType, Long bizId, Enum ...attachTypes); + + List listByMd5s(Attachment.BizType bizType, Long bizId, List> attachTypeList, List md5s) throws TimiException; + + PageResult pageByBizId(Attachment.BizType bizType, Long bizId, List> attachTypeList, Page page) throws TimiException; } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java index fff0229..8e5dcdb 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java @@ -1,15 +1,22 @@ package com.imyeyu.api.modules.common.service.implement; +import com.google.gson.Gson; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.bean.MediaAttach; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.mapper.AttachmentMapper; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.common.service.SettingService; +import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.api.modules.common.vo.attachment.AttachmentView; +import com.imyeyu.api.util.JavaCV; import com.imyeyu.io.IO; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiException; -import com.imyeyu.api.config.dbsource.TimiServerDBConfig; -import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.common.mapper.AttachmentMapper; -import com.imyeyu.api.modules.common.service.AttachmentService; -import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest; -import com.imyeyu.api.modules.common.vo.attachment.AttachmentView; +import com.imyeyu.network.Network; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; @@ -17,6 +24,8 @@ import com.mongodb.client.gridfs.GridFSBucket; import com.mongodb.client.gridfs.model.GridFSFile; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; +import org.apache.tika.Tika; import org.springframework.beans.BeanUtils; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; @@ -24,9 +33,14 @@ import org.springframework.data.mongodb.gridfs.GridFsTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.List; +import java.util.UUID; /** * @author 夜雨 @@ -37,8 +51,11 @@ import java.util.List; @RequiredArgsConstructor public class AttachmentServiceImplement extends AbstractEntityService implements AttachmentService { + private final SettingService settingService; + private final AttachmentMapper mapper; + private final Gson gson; private final GridFSBucket gridFSBucket; private final GridFsTemplate gridFsTemplate; @@ -52,11 +69,11 @@ public class AttachmentServiceImplement extends AbstractEntityService { + isImage = true; + Thumbnails.of(sourceStream).width(256).keepAspectRatio(true).toOutputStream(thumbStream); + } + case "video/mp4", "video/quicktime" -> { + log.info("capturing thumbnail: {}", request.getName()); + long start = Time.now(); + File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID().toString(), request.getName())); + try { + IO.toFile(tempFile, sourceStream); + ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2); + Thumbnails.of(IO.toInputStream(baos)).width(256).keepAspectRatio(true).toOutputStream(thumbStream); + log.info("captured thumbnail: {} at {} ms", request.getName(), Time.now() - start); + } finally { + IO.destroy(tempFile); + } + } + } + MediaAttach.ExtData extData = new MediaAttach.ExtData(); + extData.setImage(isImage); + extData.setVideo(!isImage); + extData.setSourceId(request.getId()); + extData.setSourceMongoId(request.getMongoId()); + + AttachmentRequest thumbAttach = new AttachmentRequest(); + thumbAttach.setName(Network.simpleURIFileName(request.getName()) + ".png"); + thumbAttach.setBizType(request.getBizType()); + thumbAttach.setBizId(request.getBizId()); + thumbAttach.setAttachTypeValue(MediaAttach.Type.THUMB); + thumbAttach.setExt(gson.toJson(extData)); + thumbAttach.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray())); + create(thumbAttach); + + return get(thumbAttach.getId()); + } catch (Exception e) { + log.error("create media attachment error", e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO create media attachment error"); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void deleteMedia(Long thumbId) throws TimiException { + Attachment attachment = get(thumbId); + delete(attachment.getId()); + MediaAttach.ExtData data = gson.fromJson(attachment.getExt(), MediaAttach.ExtData.class); + delete(data.getSourceId()); + } + @Override public Attachment getByBizId(Attachment.BizType bizType, long bizId) { return mapper.selectByBizId(bizType, bizId); @@ -144,7 +229,25 @@ public class AttachmentServiceImplement extends AbstractEntityService listByBizId(Attachment.BizType bizType, long bizId, Enum... attachTypes) { - return mapper.listByAttachType(bizType, bizId, attachTypes); + public List listByBizId(Attachment.BizType bizType, Long bizId, Enum... attachTypes) { + return mapper.listByBizId(bizType, bizId, Arrays.asList(attachTypes), null); + } + + @Override + public long countByBizId(Attachment.BizType bizType, Long bizId, Enum... attachTypes) { + return mapper.countByBizId(bizType, bizId, Arrays.asList(attachTypes)); + } + + @Override + public List listByMd5s(Attachment.BizType bizType, Long bizId, List> attachTypeList, List md5s) throws TimiException { + return mapper.listByMd5s(bizType, bizId, attachTypeList, md5s); + } + + @Override + public PageResult pageByBizId(Attachment.BizType bizType, Long bizId, List> attachTypeList, Page page) throws TimiException { + PageResult result = new PageResult<>(); + result.setList(mapper.listByBizId(bizType, bizId, attachTypeList, page)); + result.setTotal(mapper.countByBizId(bizType, bizId, attachTypeList)); + return result; } } diff --git a/src/main/java/com/imyeyu/api/util/JavaCV.java b/src/main/java/com/imyeyu/api/util/JavaCV.java new file mode 100644 index 0000000..bd88805 --- /dev/null +++ b/src/main/java/com/imyeyu/api/util/JavaCV.java @@ -0,0 +1,42 @@ +package com.imyeyu.api.util; + +import org.bytedeco.javacv.FFmpegFrameGrabber; +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.Java2DFrameConverter; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +/** + * + * + * @author 夜雨 + * @since 2025-10-23 17:05 + */ +public class JavaCV { + + public static ByteArrayOutputStream captureThumbnail(InputStream stream, double targetSeconds) throws Exception { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(stream)) { + grabber.start(); + long targetMillis = (long) (targetSeconds * 1000); + grabber.setTimestamp(targetMillis); + Frame frame; + while ((frame = grabber.grabImage()) != null) { + if (grabber.getTimestamp() >= targetMillis) { + Java2DFrameConverter converter = new Java2DFrameConverter(); + try (converter) { + BufferedImage bi = converter.getBufferedImage(frame); + if (bi != null) { + ImageIO.write(bi, "png", outStream); + break; + } + } + } + } + } + return outStream; + } +} diff --git a/src/main/resources/mapper/common/AttachmentMapper.xml b/src/main/resources/mapper/common/AttachmentMapper.xml index 232cceb..39ec732 100644 --- a/src/main/resources/mapper/common/AttachmentMapper.xml +++ b/src/main/resources/mapper/common/AttachmentMapper.xml @@ -2,15 +2,47 @@ attachment - SELECT * FROM - + attachment WHERE biz_type = #{bizType} - AND biz_id = #{bizId} - + + AND biz_id = #{bizId} + + + AND attach_type IN + + #{item} + + + AND deleted_at IS NULL + AND destroy_at IS NULL + + + ORDER BY + + ${key} ${value} + + + LIMIT + #{page.offset}, + #{page.limit} + + + - \ No newline at end of file + +