fix(缺陷管理): 修复富文本图片同步第三方平台问题

--bug=1036946 --user=宋昌昌 【缺陷管理】MS关联禅道,创建缺陷,缺陷内容中粘贴图片,图片没有同步到禅道上 https://www.tapd.cn/55049933/s/1476765
--bug=1036968 --user=宋昌昌 【缺陷管理】集成zentao-zentao缺陷带图片-同步到ms-未显示图片 https://www.tapd.cn/55049933/s/1476792
--bug=1037244 --user=宋昌昌 【缺陷管理】集成zentao平台,缺陷中缺陷内容包含直接粘贴的图片在zentao无法显示 https://www.tapd.cn/55049933/s/1476793
This commit is contained in:
song-cc-rock 2024-03-18 01:02:15 +08:00 committed by Craftsman
parent 70af6c3718
commit 75ee36b0ea
11 changed files with 236 additions and 13 deletions

View File

@ -20,4 +20,9 @@ public class PlatformBugDTO extends MsSyncBugDTO {
* 缺陷同步所需处理的平台自定义字段ID(同步第三方平台到MS时需要, 非默认模板时使用)
*/
private List<PlatformCustomFieldItemDTO> needSyncCustomFields;
/**
* 缺陷同步需要下载的第三方富文本图片文件Key
*/
private List<String> richTextImageKeys;
}

View File

@ -5,6 +5,9 @@ import io.metersphere.plugin.platform.dto.reponse.PlatformStatusDTO;
import lombok.Getter;
import lombok.Setter;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Getter
@ -27,4 +30,8 @@ public class PlatformBugUpdateRequest extends PlatformBugDTO {
* 第三方平台缺陷的状态
*/
private PlatformStatusDTO transitions;
/**
* MS平台缺陷富文本文件集合
*/
private Map<String, File> richFileMap = new HashMap<>();
}

View File

@ -47,6 +47,7 @@ public class DefaultRepositoryDir {
private static final String PROJECT_ENV_SSL_DIR = PROJECT_DIR + "/environment/%s";
private static final String PROJECT_FUNCTIONAL_CASE_DIR = PROJECT_DIR + "/functional-case/%s";
private static final String PROJECT_FUNCTIONAL_CASE_PREVIEW_DIR = PROJECT_DIR + "/functional-case/preview/%s";
private static final String PROJECT_BUG_PREVIEW_DIR = PROJECT_DIR + "/bug/preview/%s";
private static final String PROJECT_FILE_MANAGEMENT_DIR = PROJECT_DIR + "/file-management";
private static final String PROJECT_FILE_MANAGEMENT_PREVIEW_DIR = PROJECT_DIR + "/file-management/preview";
/**
@ -90,6 +91,10 @@ public class DefaultRepositoryDir {
return String.format(PROJECT_FUNCTIONAL_CASE_PREVIEW_DIR, projectId, functionalCaseId);
}
public static String getBugPreviewDir(String projectId, String bugId) {
return String.format(PROJECT_BUG_PREVIEW_DIR, projectId, bugId);
}
public static String getFileManagementDir(String projectId) {
return String.format(PROJECT_FILE_MANAGEMENT_DIR, projectId);
}

View File

@ -60,4 +60,7 @@ public class BugEditRequest implements Serializable {
@Schema(description = "复制的附件")
private List<BugFileDTO> copyFiles;
@Schema(description = "富文本临时文件ID")
private List<String> richTextTmpFileIds;
}

View File

@ -9,5 +9,5 @@ public enum BugAttachmentSourceType {
/**
* 缺陷内容
*/
CONTENT
RICH_TEXT
}

View File

@ -32,16 +32,16 @@ import io.metersphere.sdk.constants.LocalRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileCopyRequest;
import io.metersphere.sdk.file.FileRepository;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.FileAssociationSourceUtil;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.MsFileUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.sdk.util.*;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.mapper.BaseUserMapper;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
@ -615,4 +615,130 @@ public class BugAttachmentService {
}
return bugLocalAttachments.get(0);
}
/**
* 转存临时文件
* @param bugId 缺陷ID
* @param projectId 项目ID
* @param uploadFileIds 上传的文件ID集合
* @param userId 用户ID
* @param source 文件来源
*/
public void transferTmpFile(String bugId, String projectId, List<String> uploadFileIds, String userId, String source) {
if (org.apache.commons.collections.CollectionUtils.isEmpty(uploadFileIds)) {
return;
}
//过滤已上传过的
BugLocalAttachmentExample bugLocalAttachmentExample = new BugLocalAttachmentExample();
bugLocalAttachmentExample.createCriteria().andBugIdEqualTo(bugId).andFileIdIn(uploadFileIds).andSourceEqualTo(source);
List<BugLocalAttachment> existFiles = bugLocalAttachmentMapper.selectByExample(bugLocalAttachmentExample);
List<String> existFileIds = existFiles.stream().map(BugLocalAttachment::getFileId).distinct().toList();
List<String> fileIds = uploadFileIds.stream().filter(t -> !existFileIds.contains(t) && StringUtils.isNotBlank(t)).toList();
if (CollectionUtils.isEmpty(fileIds)) {
return;
}
// 处理本地上传文件
FileRepository defaultRepository = FileCenter.getDefaultRepository();
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
// 添加文件与功能用例的关联关系
Map<String, String> addFileMap = new HashMap<>();
LogUtils.info("开始上传副文本里的附件");
List<BugLocalAttachment> localAttachments = fileIds.stream().map(fileId -> {
BugLocalAttachment localAttachment = new BugLocalAttachment();
String fileName = getTempFileNameByFileId(fileId);
localAttachment.setId(IDGenerator.nextStr());
localAttachment.setBugId(bugId);
localAttachment.setFileId(fileId);
localAttachment.setFileName(fileName);
localAttachment.setSource(source);
long fileSize = 0;
try {
FileCopyRequest fileCopyRequest = new FileCopyRequest();
fileCopyRequest.setFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setFileName(fileName);
fileSize = defaultRepository.getFileSize(fileCopyRequest);
} catch (Exception e) {
LogUtils.error("读取文件大小失败");
}
localAttachment.setSize(fileSize);
localAttachment.setCreateUser(userId);
localAttachment.setCreateTime(System.currentTimeMillis());
addFileMap.put(fileId, fileName);
return localAttachment;
}).toList();
bugLocalAttachmentMapper.batchInsert(localAttachments);
// 上传文件到对象存储
LogUtils.info("upload to minio start");
uploadFileResource(DefaultRepositoryDir.getBugDir(projectId, bugId), addFileMap, projectId, bugId);
LogUtils.info("upload to minio end");
}
/**
* 根据文件ID查询MINIO中对应目录下的文件名称
*/
public String getTempFileNameByFileId(String fileId) {
FileRepository defaultRepository = FileCenter.getDefaultRepository();
try {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
List<String> folderFileNames = defaultRepository.getFolderFileNames(fileRequest);
if (CollectionUtils.isEmpty(folderFileNames)) {
return null;
}
String[] pathSplit = folderFileNames.get(0).split("/");
return pathSplit[pathSplit.length - 1];
} catch (Exception e) {
LogUtils.error(e);
return null;
}
}
/**
* 上传文件到资源目录
* @param folder 文件夹
* @param addFileMap 文件ID与文件名映射
* @param projectId 项目ID
* @param bugId 缺陷ID
*/
public void uploadFileResource(String folder, Map<String, String> addFileMap, String projectId, String bugId) {
if (MapUtils.isEmpty(addFileMap)) {
return;
}
FileRepository defaultRepository = FileCenter.getDefaultRepository();
for (String fileId : addFileMap.keySet()) {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
try {
String fileName = addFileMap.get(fileId);
if (StringUtils.isEmpty(fileName)) {
continue;
}
// 按ID建文件夹避免文件名重复
FileCopyRequest fileCopyRequest = new FileCopyRequest();
fileCopyRequest.setCopyFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setCopyfileName(fileName);
fileCopyRequest.setFileName(fileName);
fileCopyRequest.setFolder(folder + "/" + fileId);
// 将文件从临时目录复制到资源目录
defaultRepository.copyFile(fileCopyRequest);
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
//图片文件自动生成预览图
byte[] file = defaultRepository.getFile(fileCopyRequest);
byte[] previewImg = TempFileUtils.compressPic(file);
fileCopyRequest.setFolder(DefaultRepositoryDir.getBugPreviewDir(projectId, bugId) + "/" + fileId);
fileCopyRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileCopyRequest);
}
// 删除临时文件
fileCopyRequest.setFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setFileName(fileName);
defaultRepository.delete(fileCopyRequest);
} catch (Exception e) {
LogUtils.error("上传副文本文件失败:{}",e);
throw new MSException(Translator.get("file_upload_fail"));
}
}
}
}

View File

@ -198,6 +198,8 @@ public class BugService {
* 2. 第三方平台缺陷需调用插件同步缺陷至其他平台(自定义字段需处理);
* 3. 保存MS缺陷(基础字段, 自定义字段)
* 4. 处理附件(第三方平台缺陷需异步调用接口同步附件至第三方)
* 5. 处理富文本临时文件
* 6. 处理缺陷-用例关联关系
*/
String platformName = projectApplicationService.getPlatformName(request.getProjectId());
PlatformBugUpdateDTO platformBug = null;
@ -228,6 +230,8 @@ public class BugService {
handleAndSaveCustomFields(request, isUpdate);
// 处理附件
handleAndSaveAttachments(request, files, currentUser, platformName, platformBug);
// 处理富文本临时文件
handleRichTextTmpFile(request, bug.getId(), currentUser);
// 处理用例关联关系
handleAndSaveCaseRelation(request, isUpdate, bug, currentUser);
@ -596,6 +600,38 @@ public class BugService {
// 批量更新缺陷
updateBugs.forEach(updateBug -> {
if (CollectionUtils.isNotEmpty(updateBug.getRichTextImageKeys())) {
// 同步第三方的富文本文件
updateBug.getRichTextImageKeys().forEach(key -> {
platform.getAttachmentContent(key, (in) -> {
if (in == null) {
return;
}
String fileId = IDGenerator.nextStr();
byte[] bytes;
try {
// upload platform attachment to minio
bytes = in.readAllBytes();
FileCenter.getDefaultRepository().saveFile(bytes, buildBugFileRequest(updateBug.getProjectId(), updateBug.getId(), fileId, "image.png"));
} catch (Exception e) {
throw new MSException(e.getMessage());
}
// save bug attachment relation
BugLocalAttachment localAttachment = new BugLocalAttachment();
localAttachment.setId(IDGenerator.nextStr());
localAttachment.setBugId(updateBug.getId());
localAttachment.setFileId(fileId);
localAttachment.setFileName("image.png");
localAttachment.setSize((long) bytes.length);
localAttachment.setCreateTime(System.currentTimeMillis());
localAttachment.setCreateUser("admin");
localAttachment.setSource(BugAttachmentSourceType.RICH_TEXT.name());
bugLocalAttachmentMapper.insert(localAttachment);
// 替换富文本中的临时URL
updateBug.setDescription(updateBug.getDescription().replace("alt=\"" + key + "\"", "src=\"/attachment/download/file/" + updateBug.getProjectId() + "/" + fileId + "/true\""));
});
});
}
updateBug.setCreateUser(null);
Bug bug = new Bug();
BeanUtils.copyBean(bug, updateBug);
@ -1093,6 +1129,16 @@ public class BugService {
return uploadPlatformAttachments;
}
/**
* 处理富文本临时文件
* @param request 请求参数
* @param bugId 缺陷ID
* @param currentUser 当前用户
*/
private void handleRichTextTmpFile(BugEditRequest request, String bugId, String currentUser) {
bugAttachmentService.transferTmpFile(bugId, request.getProjectId(), request.getRichTextTmpFileIds(), currentUser, BugAttachmentSourceType.RICH_TEXT.name());
}
/**
* 处理并保存缺陷用例关联关系
* @param request 请求参数
@ -1132,6 +1178,20 @@ public class BugService {
// TITLE, DESCRIPTION 传到平台插件处理
platformRequest.setTitle(request.getTitle());
platformRequest.setDescription(request.getDescription());
if (CollectionUtils.isNotEmpty(request.getRichTextTmpFileIds())) {
request.getRichTextTmpFileIds().forEach(tmpFileId -> {
// 目前只支持富文本图片临时文件的下载, 并同步至第三方平台 (后续支持富文本其他类型文件)
FileRequest downloadRequest = buildTmpImageFileRequest(tmpFileId);
try {
byte[] tmpBytes = fileService.download(downloadRequest);
File uploadTmpFile = new File(LocalRepositoryDir.getBugTmpDir() + "/" + tmpFileId + "/" + downloadRequest.getFileName());
FileUtils.writeByteArrayToFile(uploadTmpFile, tmpBytes);
platformRequest.getRichFileMap().put(tmpFileId, uploadTmpFile);
} catch (Exception e) {
LogUtils.info("缺陷富文本临时图片文件下载失败, 文件ID: " + tmpFileId);
}
});
}
return platformRequest;
}
@ -1347,7 +1407,7 @@ public class BugService {
* @param fileName 文件名称
* @return 文件请求对象
*/
private FileRequest buildBugFileRequest(String projectId, String resourceId, String fileId, String fileName) {
public FileRequest buildBugFileRequest(String projectId, String resourceId, String fileId, String fileName) {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getBugDir(projectId, resourceId) + "/" + fileId);
fileRequest.setFileName(StringUtils.isEmpty(fileName) ? null : fileName);
@ -1355,6 +1415,20 @@ public class BugService {
return fileRequest;
}
/**
* 构建临时图片文件请求
* @param fileId 文件ID
* @return 文件请求对象
*/
private FileRequest buildTmpImageFileRequest(String fileId) {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempCompressDir() + "/" + fileId);
// 临时图片文件名称固定为image.png
fileRequest.setFileName("image.png");
fileRequest.setStorage(StorageType.MINIO.name());
return fileRequest;
}
/**
* 导出缺陷
* @param request 导出请求参数

View File

@ -47,7 +47,7 @@ export const deleteFileOrCancelAssociationUrl = '/bug/attachment/delete';
// 获取附件列表
export const getAttachmentListUrl = '/bug/attachment/list/';
// 富文本编辑器上传图片
export const editorUploadFileUrl = '/bug/attachment/upload/md/file';
export const editorUploadFileUrl = '/attachment/upload/temp/file';
// 获取回收站列表
export const getRecycleListUrl = '/bug/trash/page';

View File

@ -15,7 +15,7 @@
<MsRichText
v-if="contentEditAble"
v-model:raw="form.description"
v-model:filed-ids="fileIds"
v-model:filed-ids="descriptionFileIds"
:disabled="!contentEditAble"
:placeholder="t('editor.placeholder')"
:upload-image="handleUploadImage"
@ -238,8 +238,8 @@
const transferVisible = ref<boolean>(false);
const previewVisible = ref<boolean>(false);
const acceptType = ref('none'); // -
// id
const fileIds = ref<string[]>([]);
// -ID
const descriptionFileIds = ref<string[]>([]);
const imageUrl = ref<string>('');
const associatedDrawer = ref(false);
const fileListRef = ref<InstanceType<typeof MsFileList>>();
@ -464,6 +464,7 @@
unLinkRefIds: form.value.unLinkRefIds,
linkFileIds: form.value.linkFileIds,
customFields,
richTextTmpFileIds: descriptionFileIds.value,
};
if (!props.isPlatformDefaultTemplate) {
tmpObj.description = form.value.description;

View File

@ -36,7 +36,7 @@
<a-form-item v-if="!isPlatformDefaultTemplate" field="description" :label="t('bugManagement.edit.content')">
<MsRichText
v-model:raw="form.description"
v-model:filed-ids="richTextFileIds"
v-model:filed-ids="descriptionFileIds"
:upload-image="handleUploadImage"
/>
</a-form-item>
@ -314,7 +314,8 @@
const isPlatformDefaultTemplate = ref(false);
const imageUrl = ref('');
const previewVisible = ref<boolean>(false);
const richTextFileIds = ref<string[]>([]);
// -ID
const descriptionFileIds = ref<string[]>([]);
const visitedKey = 'doNotNextTipCreateBug';
const { getIsVisited } = useVisit(visitedKey);
@ -584,6 +585,7 @@
...form.value,
customFields,
copyFiles,
richTextTmpFileIds: descriptionFileIds.value,
};
if (isCopy.value) {
delete tmpObj.id;

View File

@ -15,7 +15,7 @@
@confirm="handleConfirm"
>
<a-form ref="formRef" class="rounded-[4px]" :model="form" layout="vertical">
<a-form-item field="PLATFORM_KEY" :label="t('project.menu.platformLabel')">
<a-form-item field="platformKey" :label="t('project.menu.platformLabel')">
<a-select
v-model="form.PLATFORM_KEY"
allow-clear