refactor gao module

This commit is contained in:
Timi
2026-08-06 10:52:50 +08:00
parent 568563475e
commit 1709316ce1
67 changed files with 1770 additions and 3319 deletions
@@ -3,8 +3,6 @@ package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
import com.imyeyu.java.TimiJava;
import com.imyeyu.network.Network;
@@ -183,30 +181,6 @@ public class AttachmentController {
return service.pageByBizId(bizType, bizId, attachTypeList, page);
}
/**
* 按业务差分更新附件
*
* @param req 更新请求
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update/biz")
public void updateByBiz(@RequestBody BizUpdateReq req) {
service.updateByBiz(req);
}
/**
* 按业务删除全部附件
*
* @param req 删除请求
*/
@AOPLog
@RequestRateLimit
@PostMapping("/delete/biz")
public void deleteByBiz(@RequestBody BizDeleteReq req) {
service.deleteByBiz(req);
}
@AOPLog
@IgnoreGlobalReturn
@GetMapping({"/read/{id}", "/download/{id}"})
@@ -1,9 +1,10 @@
package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.attachment.AccessKey;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.table.Transient;
@@ -33,7 +34,7 @@ public class Attachment extends UUIDEntity {
GAO_CUSTOMER,
GAO_REGISTRATION_EVENT_RECORD,
GAO_EVENT_RECORD,
TEMP_FILE,
@@ -54,7 +55,7 @@ public class Attachment extends UUIDEntity {
protected String mimeType;
protected JsonNode metadata;
protected ObjectNode metadata;
protected Long size;
@@ -82,6 +83,14 @@ public class Attachment extends UUIDEntity {
@Transient
protected MultipartFile file;
public String buildMongoName() {
StringBuilder result = new StringBuilder(bizType.name());
if (TimiJava.isNotEmpty(attachType)) {
result.append('_').append(attachType.toUpperCase());
}
return result.append('_').append(name).toString();
}
public InputStream openInputStream() {
AttachmentService service = TimiServerAPI.applicationContext.getBean(AttachmentService.class);
return service.getInputStreamByMongoId(mongoId);
@@ -4,9 +4,9 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.mapper.RawMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
/**
@@ -32,7 +32,7 @@ public interface AttachmentMapper extends BaseMapper<Attachment, String>, RawMap
List<Attachment> selectByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes, Page<Attachment> page);
List<Attachment> selectByBizIdList(@Param("bizType") Attachment.BizType bizType, @Param("bizIdList") List<String> bizIdList, @Param("attachTypes") List<String> attachTypes);
List<Attachment> selectByBizIdList(Attachment.BizType bizType, Collection<String> bizIdList, List<String> attachTypes);
long countByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes);
@@ -1,13 +1,12 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -19,6 +18,13 @@ import java.util.Map;
*/
public interface AttachmentService extends BaseService<Attachment, String> {
///
/// 克隆附件,复制业务属性和文件内容并生成新的附件记录
///
/// @param id 源附件 ID
/// @return 克隆后的附件
Attachment clone(String id);
/**
* 按需获取或创建缩略图附件。
* <p>当附件为图片或视频时,若指定尺寸的缩略图已存在则直接返回缓存,
@@ -52,14 +58,14 @@ public interface AttachmentService extends BaseService<Attachment, String> {
List<Attachment> listByBizId(Attachment.BizType bizType, String bizId, String... attachTypes);
/**
* 根据多个业务 ID 批量获取附件
* 根据多个业务 ID 批量获取附件
*
* @param bizType 业务类型
* @param bizIdList 业务 ID 列表
* @param attachTypes 附件类型,可为 null
* @return 按业务 ID 分组的附件列表
*/
Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, List<String> bizIdList, String... attachTypes);
Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, Collection<String> bizIdList, String... attachTypes);
/**
* 根据业务获取所有附件
@@ -84,10 +90,8 @@ public interface AttachmentService extends BaseService<Attachment, String> {
* <li>请求有、数据库有、无 tempFileId → 仅更新元数据</li>
* <li>请求有、数据库有、有 tempFileId → 更新文件与元数据</li>
* </ul>
*
* @param req 差分更新请求
*/
void updateByBiz(BizUpdateReq req);
void updateByBizId(Attachment.BizType bizType, String bizId, List<Attachment> list);
void deleteByBiz(BizDeleteReq req);
void deleteByBizId(Attachment.BizType bizType, String bizId);
}
@@ -8,8 +8,6 @@ import com.imyeyu.api.modules.common.bean.attachment.Metadata;
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.attach.BizDeleteReq;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.util.JavaCV;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
@@ -43,7 +41,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -71,6 +69,88 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(Attachment attachment) {
TimiException.required(attachment.getBizType(), "not found String");
TimiException.required(attachment.getName(), "not found attachment.name");
String mongoId = null;
try {
if (attachment.getFile() != null) {
attachment.setInputStream(attachment.getFile().getInputStream());
}
InputStream is = attachment.getInputStream();
TimiException.required(is, "not found attachment.inputStream");
mongoId = gridFsTemplate.store(attachment.getInputStream(), attachment.buildMongoName()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
attachment.setMongoId(mongoId);
attachment.setSize(gridFSFile.getLength());
attachment.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setMimeType(new Tika().detect(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setUploaderIp(TimiSpring.getRequestIP());
attachment.setIsDestroyed(false);
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
ObjectNode metadata = TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode());
metadata.put("width", image.getWidth());
metadata.put("height", image.getHeight());
attachment.setMetadata(metadata);
}
mapper.insert(attachment);
} catch (Exception e) {
if (mongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
}
log.error("create error", e);
throw new TimiException(TimiCode.ARG_BAD, "TODO read attachment input stream error");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(Attachment attachment) {
String newMongoId = null;
try {
if (attachment.getFile() != null) {
attachment.setInputStream(attachment.getFile().getInputStream());
}
InputStream stream = attachment.getInputStream();
if (stream == null) {
// 无新文件,仅更新元数据字段
super.update(attachment);
return;
}
// 有新文件时,先查旧记录获取旧 mongoId 和兜底字段
Attachment old = mapper.selectRaw(attachment.getId());
// 存储新文件
newMongoId = gridFsTemplate.store(stream, attachment.buildMongoName()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(newMongoId)));
attachment.setMongoId(newMongoId);
attachment.setSize(gridFSFile.getLength());
attachment.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setMimeType(new Tika().detect(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
ObjectNode metadata = TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode());
metadata.put("width", image.getWidth());
metadata.put("height", image.getHeight());
attachment.setMetadata(metadata);
}
// 更新数据库后删除旧 GridFS 文件
super.update(attachment);
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(old.getMongoId())));
} catch (Exception e) {
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
if (TimiJava.isNotEmpty(newMongoId)) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(newMongoId)));
}
log.error("update error", e);
throw new TimiException(TimiCode.ERROR, "TODO update file error");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
@@ -108,101 +188,30 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(Attachment attachment) {
TimiException.required(attachment.getBizType(), "not found String");
TimiException.required(attachment.getName(), "not found attachment.name");
String mongoId = null;
try {
if (attachment.getFile() != null) {
attachment.setInputStream(attachment.getFile().getInputStream());
}
InputStream is = attachment.getInputStream();
TimiException.required(is, "not found attachment.inputStream");
public Attachment clone(String id) {
TimiException.required(id, "not found id");
Attachment source = mapper.selectRaw(id);
TimiException.required(source, "not found attachment");
TimiException.required(source.getMongoId(), "not found mongoId");
StringBuilder mongoName = new StringBuilder(attachment.getBizType().name());
if (TimiJava.isNotEmpty(attachment.getAttachType())) {
mongoName.append("_").append(attachment.getAttachType().toUpperCase()).append("_");
Attachment target = new Attachment();
target.setBizType(source.getBizType());
target.setBizId(source.getBizId());
target.setAttachType(source.getAttachType());
target.setTitle(source.getTitle());
target.setName(source.getName());
target.setMetadata(source.getMetadata() == null ? null : source.getMetadata().deepCopy());
target.setDestroyAt(source.getDestroyAt());
try (InputStream inputStream = getInputStreamByMongoId(source.getMongoId())) {
target.setInputStream(inputStream);
create(target);
return target;
} catch (IOException e) {
if (target.getMongoId() != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(target.getMongoId())));
}
mongoName.append(attachment.getName());
mongoId = gridFsTemplate.store(attachment.getInputStream(), mongoName.toString()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
attachment.setMongoId(mongoId);
attachment.setSize(gridFSFile.getLength());
attachment.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setMimeType(new Tika().detect(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setUploaderIp(TimiSpring.getRequestIP());
attachment.setIsDestroyed(false);
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode()));
if (attachment.getMetadata() instanceof ObjectNode obj) {
obj.put("width", image.getWidth());
obj.put("height", image.getHeight());
}
}
mapper.insert(attachment);
} catch (Exception e) {
if (mongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
}
log.error("create error", e);
throw new TimiException(TimiCode.ARG_BAD, "TODO read attachment input stream error");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(Attachment attachment) {
String newMongoId = null;
try {
if (attachment.getFile() != null) {
attachment.setInputStream(attachment.getFile().getInputStream());
}
InputStream stream = attachment.getInputStream();
if (stream == null) {
// 无新文件,仅更新元数据字段
super.update(attachment);
return;
}
// 有新文件时,先查旧记录获取旧 mongoId 和兜底字段
Attachment old = mapper.selectRaw(attachment.getId());
// 构建存储名,优先取调用方设置的字段,否则沿用旧值
Attachment.BizType bizType = TimiJava.defaultIfNull(attachment.getBizType(), old.getBizType());
String attachType = TimiJava.defaultIfEmpty(attachment.getAttachType(), old.getAttachType());
String name = TimiJava.defaultIfEmpty(attachment.getName(), old.getName());
StringBuilder mongoName = new StringBuilder(bizType.name());
if (TimiJava.isNotEmpty(attachType)) {
mongoName.append("_").append(attachType.toUpperCase()).append("_");
}
mongoName.append(name);
// 存储新文件
newMongoId = gridFsTemplate.store(stream, mongoName.toString()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(newMongoId)));
attachment.setMongoId(newMongoId);
attachment.setSize(gridFSFile.getLength());
attachment.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setMimeType(new Tika().detect(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode()));
if (attachment.getMetadata() instanceof ObjectNode obj) {
obj.put("width", image.getWidth());
obj.put("height", image.getHeight());
}
}
// 更新数据库后删除旧 GridFS 文件
super.update(attachment);
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(old.getMongoId())));
} catch (Exception e) {
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
if (newMongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(newMongoId)));
}
log.error("update error", e);
throw new TimiException(TimiCode.ERROR, "TODO update file error");
log.error("clone attachment error, id={}", id, e);
throw new TimiException(TimiCode.ERROR, "clone attachment error", e);
}
}
@@ -216,9 +225,9 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
if (!isImage && !isVideo) {
return null;
}
// 命中缓存直接返回
Attachment cached = mapper.selectThumb(source.getBizType(), source.getId(), requestWidth, requestHeight);
if (cached != null) {
// 命中缓存直接返回
return cached;
}
try {
@@ -313,19 +322,8 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
@Override
public Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, List<String> bizIdList, String... attachTypes) {
if (bizIdList == null || bizIdList.isEmpty()) {
return Map.of();
}
List<String> normalizedBizIdList = bizIdList.stream()
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
if (normalizedBizIdList.isEmpty()) {
return Map.of();
}
return mapper.selectByBizIdList(bizType, new ArrayList<>(normalizedBizIdList), List.of(attachTypes)).stream()
.collect(Collectors.groupingBy(Attachment::getBizId));
public Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, Collection<String> bizIdList, String... attachTypes) {
return mapper.selectByBizIdList(bizType, bizIdList, List.of(attachTypes)).stream().collect(Collectors.groupingBy(Attachment::getBizId));
}
@Override
@@ -348,16 +346,12 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void updateByBiz(BizUpdateReq req) {
Attachment.BizType bizType = req.getBizType();
String bizId = req.getBizId();
List<Attachment> items = req.getItems();
public void updateByBizId(Attachment.BizType bizType, String bizId, List<Attachment> list) {
// 数据库现有附件
List<Attachment> dbList = mapper.selectAllByBizId(bizType, bizId);
Set<String> dbIds = dbList.stream().map(Attachment::getId).collect(Collectors.toSet());
// 请求附件 ID
Set<String> reqIds = items.stream().map(Attachment::getId).filter(Objects::nonNull).collect(Collectors.toSet());
Set<String> reqIds = list.stream().map(Attachment::getId).filter(Objects::nonNull).collect(Collectors.toSet());
// 数据库有、请求无:删除
for (Attachment attach : dbList) {
if (!reqIds.contains(attach.getId())) {
@@ -365,7 +359,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
}
for (Attachment item : items) {
for (Attachment item : list) {
boolean isNew = item.getId() == null || !dbIds.contains(item.getId());
String tempFileId = item.getTempFileId();
@@ -432,8 +426,8 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteByBiz(BizDeleteReq req) {
List<Attachment> attachmentList = mapper.selectAllByBizId(req.getBizType(), req.getBizId());
public void deleteByBizId(Attachment.BizType bizType, String bizId) {
List<Attachment> attachmentList = mapper.selectAllByBizId(bizType, bizId);
for (Attachment attachment : attachmentList) {
delete(attachment.getId());
}
@@ -1,20 +0,0 @@
package com.imyeyu.api.modules.common.vo.attach;
import com.imyeyu.api.modules.common.entity.Attachment;
import lombok.Data;
/**
* 业务附件差分更新请求
*
* @author 夜雨
* @since 2026-03-11 00:00
*/
@Data
public class BizDeleteReq {
/** 业务类型 */
private Attachment.BizType bizType;
/** 业务 ID */
private String bizId;
}
@@ -1,26 +0,0 @@
package com.imyeyu.api.modules.common.vo.attach;
import com.imyeyu.api.modules.common.entity.Attachment;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 业务附件差分更新请求
*
* @author 夜雨
* @since 2026-03-11 00:00
*/
@Data
public class BizUpdateReq {
/** 业务类型 */
private Attachment.BizType bizType;
/** 业务 ID */
private String bizId;
/** 附件项列表 */
private List<Attachment> items = new ArrayList<>();
}