support add media thumb attachment

This commit is contained in:
Timi
2025-11-06 14:47:48 +08:00
parent c270ae177d
commit 2ba868b3b6
7 changed files with 318 additions and 40 deletions

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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<Attachment, Long> {
@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<Attachment> listByBizId(Attachment.BizType bizType, long bizId, long offset, int limit);
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, Page page);
List<Attachment> listByAttachType(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
long countByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes);
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, List<String> md5s);
}

View File

@ -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<Attachment, Long>, DeletableService<Long>, DestroyableService<Long> {
public interface AttachmentService extends GettableService<Attachment, Long>, PageableService<Attachment>, UpdatableService<Attachment>, DeletableService<Long>, DestroyableService<Long> {
/**
*
@ -29,6 +33,16 @@ public interface AttachmentService extends GettableService<Attachment, Long>, 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);
@ -47,10 +61,25 @@ public interface AttachmentService extends GettableService<Attachment, Long>, De
* 根据业务获取所有附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypes
* @param bizId 业务 ID,可为 null
* @param attachTypes 附件类型,可为 null
* @return 所有附件
* @throws TimiException 服务异常
*/
List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
List<Attachment> 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<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s) throws TimiException;
PageResult<Attachment> pageByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, Page page) throws TimiException;
}

View File

@ -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<Attachment, Long> 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<Attachment
public void destroy(Long id) {
try {
Attachment attachment = get(id);
if (!attachment.isDeleted()) {
delete(id);
}
mapper.destroy(attachment.getId());
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId())));
attachment.setDeletedAt(Time.now());
attachment.setDestroyAt(Time.now());
mapper.update(attachment);
} catch (Exception e) {
log.error("delete mongo file error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error");
@ -81,12 +98,12 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
mongoName.append(request.getName());
mongoId = gridFsTemplate.store(is, mongoName.toString()).toString();
Attachment attachment = new Attachment();
BeanUtils.copyProperties(request, attachment);
attachment.setMongoId(mongoId);
attachment.setCreatedAt(Time.now());
mapper.insert(attachment);
mongoId = gridFsTemplate.store(request.getInputStream(), mongoName.toString()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
request.setMongoId(mongoId);
request.setSize(gridFSFile.getLength());
request.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
mapper.insert(request);
} catch (Exception e) {
if (mongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
@ -96,6 +113,74 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public Attachment createMedia(AttachmentRequest request) throws TimiException {
TimiException.required(request.getName(), "not found name");
TimiException.required(request.getBizType(), "not found bizType");
TimiException.required(request.getInputStream(), "not found inputStream");
try {
// 储存源文件
request.setAttachTypeValue(MediaAttach.Type.SOURCE);
create(request);
// 生成缩略图
InputStream mimeStream = getInputStreamByMongoId(request.getMongoId());
String mimeType = new Tika().detect(mimeStream);
boolean isImage = false;
InputStream sourceStream = getInputStreamByMongoId(request.getMongoId());
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
switch (mimeType) {
case "image/png", "image/bmp", "image/jpeg" -> {
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<Attachment
}
@Override
public List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, Enum<?>... attachTypes) {
return mapper.listByAttachType(bizType, bizId, attachTypes);
public List<Attachment> 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<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s) throws TimiException {
return mapper.listByMd5s(bizType, bizId, attachTypeList, md5s);
}
@Override
public PageResult<Attachment> pageByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, Page page) throws TimiException {
PageResult<Attachment> result = new PageResult<>();
result.setList(mapper.listByBizId(bizType, bizId, attachTypeList, page));
result.setTotal(mapper.countByBizId(bizType, bizId, attachTypeList));
return result;
}
}

View File

@ -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;
}
}

View File

@ -2,15 +2,66 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.common.mapper.AttachmentMapper">
<sql id="table">attachment</sql>
<select id="listByAttachType" resultType="com.imyeyu.api.modules.common.entity.Attachment">
<select id="listByBizId" resultType="com.imyeyu.api.modules.common.entity.Attachment">
SELECT
*
FROM
<include refid="table" />
attachment
WHERE
biz_type = #{bizType}
<if test="bizId != null">
AND biz_id = #{bizId}
<if test="attachTypes != null and 0 &lt; attachTypes.length">
</if>
<if test="attachTypes != null and 0 &lt; attachTypes.size()">
AND attach_type IN
<foreach collection="attachTypes" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
AND deleted_at IS NULL
AND destroy_at IS NULL
<if test="page != null">
<if test="page.orderMap != null and !page.orderMap.isEmpty()">
ORDER BY
<foreach collection="page.orderMap" index="key" item="value" separator=",">
${key} ${value}
</foreach>
</if>
LIMIT
#{page.offset},
#{page.limit}
</if>
</select>
<select id="countByBizId" resultType="long">
SELECT
COUNT(1)
FROM
attachment
WHERE
biz_type = #{bizType}
<if test="bizId != null">
AND biz_id = #{bizId}
</if>
<if test="attachTypes != null and 0 &lt; attachTypes.size()">
AND attach_type IN
<foreach collection="attachTypes" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
AND deleted_at IS NULL
AND destroy_at IS NULL
</select>
<select id="listByMd5s" resultType="com.imyeyu.api.modules.common.entity.Attachment">
SELECT
*
FROM
attachment
WHERE
biz_type = #{bizType}
<if test="bizId != null">
AND biz_id = #{bizId}
</if>
<if test="attachTypes != null and 0 &lt; attachTypes.size()">
AND attach_type IN
<foreach collection="attachTypes" item="item" open="(" separator="," close=")">
#{item}