feat(测试跟踪): 同步缺陷支持时间范围内同步

--story=1010094 --user=宋昌昌 同步缺陷支持同步指定时间范围内的缺陷 https://www.tapd.cn/55049933/s/1269234
This commit is contained in:
song-cc-rock 2022-10-20 11:18:21 +08:00 committed by f2c-ci-robot[bot]
parent 50af3dd418
commit 15abe5a6d1
17 changed files with 237 additions and 45 deletions

View File

@ -613,6 +613,7 @@ const message = {
system_template: 'System Template', system_template: 'System Template',
option_check: 'Please add option values', option_check: 'Please add option values',
option_value_check: 'Please fill in the full option values', option_value_check: 'Please fill in the full option values',
sync_issue_tips: 'Note: The system will automatically synchronize at 00:00:00 every day',
}, },
workspace: { workspace: {
id: 'Workspace ID', id: 'Workspace ID',

View File

@ -619,6 +619,7 @@ const message = {
system_template: '系统模板', system_template: '系统模板',
option_check: '请添加选项值', option_check: '请添加选项值',
option_value_check: '请填写完整选项值', option_value_check: '请填写完整选项值',
sync_issue_tips: '注: 系统在每天00:00:00会自动同步一次',
}, },
workspace: { workspace: {
id: '工作空间ID', id: '工作空间ID',

View File

@ -616,6 +616,7 @@ const message = {
system_template: '系統模板', system_template: '系統模板',
option_check: '請添加選項值', option_check: '請添加選項值',
option_value_check: '請填寫完整選項值', option_value_check: '請填寫完整選項值',
sync_issue_tips: '注系統在每天00:00:00會自動同步一次',
}, },
workspace: { workspace: {
id: '工作空間ID', id: '工作空間ID',

View File

@ -1,19 +1,17 @@
package io.metersphere.xpack.track.controller; package io.metersphere.xpack.track.controller;
import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.xpack.track.dto.IssueSyncRequest;
import io.metersphere.xpack.track.service.XpackIssueService; import io.metersphere.xpack.track.service.XpackIssueService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping(value = "/xpack/issue") @RequestMapping(value = "/xpack/issue")
public class XpackIssueController { public class XpackIssueController {
@GetMapping("/sync/{projectId}") @PostMapping("/sync")
public boolean getPlatformIssue(@PathVariable String projectId) { public boolean getPlatformIssue(@RequestBody IssueSyncRequest request) {
XpackIssueService xpackIssueService = CommonBeanFactory.getBean(XpackIssueService.class); XpackIssueService xpackIssueService = CommonBeanFactory.getBean(XpackIssueService.class);
return xpackIssueService.syncThirdPartyIssues(projectId); return xpackIssueService.syncThirdPartyIssues(request);
} }
} }

View File

@ -0,0 +1,25 @@
package io.metersphere.xpack.track.dto;
import lombok.Data;
/**
* @author songcc
*/
@Data
public class IssueSyncRequest {
/**
* 项目ID
*/
private String projectId;
/**
* 缺陷创建时间
*/
private Long createTime;
/**
* TRUE: 创建时间之前
*/
private boolean pre;
}

View File

@ -43,6 +43,7 @@ public class IssuesUpdateRequest extends IssuesWithBLOBs {
* azure devops bug同步fields * azure devops bug同步fields
*/ */
private String devopsFields; private String devopsFields;
private String azureDevopsCreateTime;
private PlatformStatusDTO transitions; private PlatformStatusDTO transitions;

View File

@ -70,7 +70,7 @@ public interface IssuesPlatform {
* 同步缺陷全量的缺陷 * 同步缺陷全量的缺陷
* @param project * @param project
*/ */
void syncAllIssues(Project project); void syncAllIssues(Project project, IssueSyncRequest syncRequest);
/** /**
* 获取第三方平台缺陷模板 * 获取第三方平台缺陷模板

View File

@ -1,6 +1,10 @@
package io.metersphere.xpack.track.service; package io.metersphere.xpack.track.service;
import io.metersphere.xpack.track.dto.IssueSyncRequest;
public interface XpackIssueService { public interface XpackIssueService {
boolean syncThirdPartyIssues(String projectId); boolean syncThirdPartyIssues(IssueSyncRequest request);
void syncThirdPartyIssues();
} }

View File

@ -112,7 +112,7 @@
group by issues.id group by issues.id
</select> </select>
<select id="getIssueForSync" resultType="io.metersphere.xpack.track.dto.IssuesDao"> <select id="getIssueForSync" resultType="io.metersphere.xpack.track.dto.IssuesDao">
select id,platform, platform_id select id, platform, platform_id, create_time
from issues from issues
where project_id = #{projectId} and platform = #{platform} and (platform_status != 'delete' or platform_status is null); where project_id = #{projectId} and platform = #{platform} and (platform_status != 'delete' or platform_status is null);
</select> </select>

View File

@ -136,9 +136,9 @@ public class IssuesController {
return issuesService.getZentaoBuilds(request); return issuesService.getZentaoBuilds(request);
} }
@GetMapping("/sync/{projectId}") @PostMapping("/sync")
public boolean getPlatformIssue(@PathVariable String projectId) { public boolean getPlatformIssue(@RequestBody IssueSyncRequest request) {
return issuesService.syncThirdPartyIssues(projectId); return issuesService.syncThirdPartyIssues(request);
} }
@GetMapping("/sync/check/{projectId}") @GetMapping("/sync/check/{projectId}")

View File

@ -0,0 +1,35 @@
package io.metersphere.job.schedule;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.sechedule.MsScheduleJob;
import io.metersphere.service.IssuesService;
import io.metersphere.xpack.license.dto.LicenseDTO;
import io.metersphere.xpack.license.service.LicenseService;
import io.metersphere.xpack.track.service.XpackIssueService;
import org.quartz.JobExecutionContext;
/**
* @author songcc
*/
public class IssueSyncJob extends MsScheduleJob {
private LicenseService licenseService;
private XpackIssueService xpackIssueService;
private IssuesService issuesService;
public IssueSyncJob() {
licenseService = CommonBeanFactory.getBean(LicenseService.class);
xpackIssueService = CommonBeanFactory.getBean(XpackIssueService.class);
issuesService = CommonBeanFactory.getBean(IssuesService.class);
}
@Override
public void businessExecute(JobExecutionContext context) {
LicenseDTO licenseDTO = licenseService.validate();
if (licenseDTO != null && licenseDTO.getLicense() != null) {
xpackIssueService.syncThirdPartyIssues();
} else {
issuesService.syncThirdPartyIssues();
}
}
}

View File

@ -14,10 +14,10 @@ import io.metersphere.commons.utils.*;
import io.metersphere.plan.service.TestPlanTestCaseService; import io.metersphere.plan.service.TestPlanTestCaseService;
import io.metersphere.utils.DistinctKeyUtil; import io.metersphere.utils.DistinctKeyUtil;
import io.metersphere.xpack.track.dto.AttachmentSyncType; import io.metersphere.xpack.track.dto.AttachmentSyncType;
import io.metersphere.xpack.track.dto.*;
import io.metersphere.constants.AttachmentType; import io.metersphere.constants.AttachmentType;
import io.metersphere.constants.SystemCustomField; import io.metersphere.constants.SystemCustomField;
import io.metersphere.dto.CustomFieldDao; import io.metersphere.dto.CustomFieldDao;
import io.metersphere.xpack.track.dto.IssueTemplateDao;
import io.metersphere.i18n.Translator; import io.metersphere.i18n.Translator;
import io.metersphere.log.utils.ReflexObjectUtil; import io.metersphere.log.utils.ReflexObjectUtil;
import io.metersphere.log.vo.DetailColumn; import io.metersphere.log.vo.DetailColumn;
@ -34,19 +34,15 @@ import io.metersphere.request.issues.JiraIssueTypeRequest;
import io.metersphere.request.issues.PlatformIssueTypeRequest; import io.metersphere.request.issues.PlatformIssueTypeRequest;
import io.metersphere.request.testcase.AuthUserIssueRequest; import io.metersphere.request.testcase.AuthUserIssueRequest;
import io.metersphere.request.testcase.IssuesCountRequest; import io.metersphere.request.testcase.IssuesCountRequest;
import io.metersphere.xpack.track.dto.PlatformUser;
import io.metersphere.service.issue.domain.jira.JiraIssueType; import io.metersphere.service.issue.domain.jira.JiraIssueType;
import io.metersphere.service.issue.domain.zentao.ZentaoBuild; import io.metersphere.service.issue.domain.zentao.ZentaoBuild;
import io.metersphere.request.attachment.AttachmentRequest; import io.metersphere.request.attachment.AttachmentRequest;
import io.metersphere.xpack.track.dto.DemandDTO;
import io.metersphere.xpack.track.dto.IssuesDao;
import io.metersphere.xpack.track.dto.request.IssuesRequest; import io.metersphere.xpack.track.dto.request.IssuesRequest;
import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest; import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest;
import io.metersphere.service.issue.platform.*; import io.metersphere.service.issue.platform.*;
import io.metersphere.service.remote.project.TrackCustomFieldTemplateService; import io.metersphere.service.remote.project.TrackCustomFieldTemplateService;
import io.metersphere.service.remote.project.TrackIssueTemplateService; import io.metersphere.service.remote.project.TrackIssueTemplateService;
import io.metersphere.service.wapper.TrackProjectService; import io.metersphere.service.wapper.TrackProjectService;
import io.metersphere.xpack.track.dto.PlatformStatusDTO;
import io.metersphere.xpack.track.issue.IssuesPlatform; import io.metersphere.xpack.track.issue.IssuesPlatform;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
@ -655,7 +651,9 @@ public class IssuesService {
List<String> projectIds = trackProjectService.getThirdPartProjectIds(); List<String> projectIds = trackProjectService.getThirdPartProjectIds();
projectIds.forEach(id -> { projectIds.forEach(id -> {
try { try {
syncThirdPartyIssues(id); IssueSyncRequest request = new IssueSyncRequest();
request.setProjectId(id);
syncThirdPartyIssues(request);
} catch (Exception e) { } catch (Exception e) {
LogUtil.error(e.getMessage(), e); LogUtil.error(e.getMessage(), e);
} }
@ -698,29 +696,32 @@ public class IssuesService {
stringRedisTemplate.delete(SYNC_THIRD_PARTY_ISSUES_KEY + ":" + projectId); stringRedisTemplate.delete(SYNC_THIRD_PARTY_ISSUES_KEY + ":" + projectId);
} }
public boolean syncThirdPartyIssues(String projectId) { public boolean syncThirdPartyIssues(IssueSyncRequest syncRequest) {
if (StringUtils.isNotBlank(projectId)) { if (StringUtils.isNotBlank(syncRequest.getProjectId())) {
String syncValue = getSyncKey(projectId); String syncValue = getSyncKey(syncRequest.getProjectId());
if (StringUtils.isNotEmpty(syncValue)) { if (StringUtils.isNotEmpty(syncValue)) {
return false; return false;
} }
setSyncKey(projectId); setSyncKey(syncRequest.getProjectId());
Project project = baseProjectService.getProjectById(projectId); Project project = baseProjectService.getProjectById(syncRequest.getProjectId());
List<IssuesDao> issues = extIssuesMapper.getIssueForSync(projectId, project.getPlatform()); List<IssuesDao> issues = extIssuesMapper.getIssueForSync(syncRequest.getProjectId(), project.getPlatform());
if (syncRequest.getCreateTime() != null) {
issues = filterSyncIssuesByCreated(issues, syncRequest);
}
if (CollectionUtils.isEmpty(issues)) { if (CollectionUtils.isEmpty(issues)) {
return true; return true;
} }
IssuesRequest issuesRequest = new IssuesRequest(); IssuesRequest issuesRequest = new IssuesRequest();
issuesRequest.setProjectId(projectId); issuesRequest.setProjectId(syncRequest.getProjectId());
issuesRequest.setWorkspaceId(project.getWorkspaceId()); issuesRequest.setWorkspaceId(project.getWorkspaceId());
try { try {
if (!trackProjectService.isThirdPartTemplate(project)) { if (!trackProjectService.isThirdPartTemplate(project)) {
String defaultCustomFields = getDefaultCustomFields(projectId); String defaultCustomFields = getDefaultCustomFields(syncRequest.getProjectId());
issuesRequest.setDefaultCustomFields(defaultCustomFields); issuesRequest.setDefaultCustomFields(defaultCustomFields);
} }
IssuesPlatform platform = IssueFactory.createPlatform(project.getPlatform(), issuesRequest); IssuesPlatform platform = IssueFactory.createPlatform(project.getPlatform(), issuesRequest);
@ -728,7 +729,7 @@ public class IssuesService {
} catch (Exception e) { } catch (Exception e) {
throw e; throw e;
} finally { } finally {
deleteSyncKey(projectId); deleteSyncKey(syncRequest.getProjectId());
} }
} }
return true; return true;
@ -817,7 +818,6 @@ public class IssuesService {
public void calculatePlanReport(String planId, TestPlanSimpleReportDTO report) { public void calculatePlanReport(String planId, TestPlanSimpleReportDTO report) {
List<PlanReportIssueDTO> planReportIssueDTOS = extIssuesMapper.selectForPlanReport(planId); List<PlanReportIssueDTO> planReportIssueDTOS = extIssuesMapper.selectForPlanReport(planId);
planReportIssueDTOS = DistinctKeyUtil.distinctByKey(planReportIssueDTOS, PlanReportIssueDTO::getId);
TestPlanFunctionResultReportDTO functionResult = report.getFunctionResult(); TestPlanFunctionResultReportDTO functionResult = report.getFunctionResult();
List<TestCaseReportStatusResultDTO> statusResult = new ArrayList<>(); List<TestCaseReportStatusResultDTO> statusResult = new ArrayList<>();
Map<String, TestCaseReportStatusResultDTO> statusResultMap = new HashMap<>(); Map<String, TestCaseReportStatusResultDTO> statusResultMap = new HashMap<>();
@ -1087,4 +1087,15 @@ public class IssuesService {
MSException.throwException(Translator.get("issue_project_not_exist")); MSException.throwException(Translator.get("issue_project_not_exist"));
} }
} }
private List<IssuesDao> filterSyncIssuesByCreated(List<IssuesDao> issues, IssueSyncRequest syncRequest) {
List<IssuesDao> filterIssues = issues.stream().filter(issue -> {
if (syncRequest.isPre()) {
return issue.getCreateTime() <= syncRequest.getCreateTime();
} else {
return issue.getCreateTime() >= syncRequest.getCreateTime();
}
}).collect(Collectors.toList());
return filterIssues;
}
} }

View File

@ -11,6 +11,7 @@ import io.metersphere.commons.constants.IssuesStatus;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.*; import io.metersphere.commons.utils.*;
import io.metersphere.dto.CustomFieldItemDTO; import io.metersphere.dto.CustomFieldItemDTO;
import io.metersphere.xpack.track.dto.IssueSyncRequest;
import io.metersphere.xpack.track.dto.IssueTemplateDao; import io.metersphere.xpack.track.dto.IssueTemplateDao;
import io.metersphere.xpack.track.dto.PlatformStatusDTO; import io.metersphere.xpack.track.dto.PlatformStatusDTO;
import io.metersphere.dto.UserDTO; import io.metersphere.dto.UserDTO;
@ -486,7 +487,7 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
} }
@Override @Override
public void syncAllIssues(Project project) {} public void syncAllIssues(Project project, IssueSyncRequest syncRequest) {}
@Override @Override
public IssueTemplateDao getThirdPartTemplate() {return null;} public IssueTemplateDao getThirdPartTemplate() {return null;}

View File

@ -0,0 +1,2 @@
-- 同步缺陷定时器修改
update schedule set value = '0 0 0 * * ?', job = 'io.metersphere.job.schedule.IssueSyncJob' where id = '7a23d4db-9909-438d-9e36-58e432c8c4ae'

View File

@ -132,33 +132,33 @@ export function getRelateIssues(page) {
}); });
} }
export function syncIssues() { export function syncIssues(param) {
let url = 'issues/sync/'; let url = 'issues/sync';
if (hasLicense()) { if (hasLicense()) {
url = 'xpack/issue/sync/'; url = 'xpack/issue/sync';
} }
// 浏览器默认策略请求同一个url可能导致 stalled 时间过长加个uuid防止请求阻塞 // 浏览器默认策略请求同一个url可能导致 stalled 时间过长加个uuid防止请求阻塞
url = url + getCurrentProjectID() + "?stamp=" + getUUID(); url = url + "?stamp=" + getUUID();
return get(url); return post(url, param);
} }
// 轮询同步状态 // 轮询同步状态
export function checkSyncIssues(result, isNotFirst) { export function checkSyncIssues(loading, isNotFirst) {
let url = 'issues/sync/check/' + getCurrentProjectID() + "?stamp=" + getUUID(); let url = 'issues/sync/check/' + getCurrentProjectID() + "?stamp=" + getUUID();
return get(url) return get(url)
.then((response) => { .then((response) => {
if (response.data === false) { if (response.data === false) {
if (result.loading === true) { if (loading === true) {
if (!isNotFirst) { if (!isNotFirst) {
// 第一次才提示 // 第一次才提示
$warning(i18n.t('test_track.issue.issue_sync_tip')); $warning(i18n.t('test_track.issue.issue_sync_tip'));
} }
setTimeout(() => checkSyncIssues(result, true), 1000); setTimeout(() => checkSyncIssues(loading, true), 1000);
} }
} else { } else {
if (result.loading === true) { if (loading === true) {
$success(i18n.t('test_track.issue.sync_complete')); $success(i18n.t('test_track.issue.sync_complete'));
result.loading = false; loading = false;
} }
} }
}); });

View File

@ -17,7 +17,7 @@
</template> </template>
<ms-table <ms-table
v-loading="page.result.loading" v-loading="page.result.loading || loading"
:data="page.data" :data="page.data"
:enableSelection="false" :enableSelection="false"
:condition="page.condition" :condition="page.condition"
@ -165,6 +165,7 @@
:total="page.total"/> :total="page.total"/>
<issue-edit @refresh="getIssues" ref="issueEdit"/> <issue-edit @refresh="getIssues" ref="issueEdit"/>
<issue-sync-select @syncConfirm="syncConfirm" ref="issueSyncSelect" />
</el-card> </el-card>
</ms-main-container> </ms-main-container>
</ms-container> </ms-container>
@ -185,6 +186,7 @@ import {
import MsTableHeader from "metersphere-frontend/src/components/MsTableHeader"; import MsTableHeader from "metersphere-frontend/src/components/MsTableHeader";
import IssueDescriptionTableItem from "@/business/issue/IssueDescriptionTableItem"; import IssueDescriptionTableItem from "@/business/issue/IssueDescriptionTableItem";
import IssueEdit from "@/business/issue/IssueEdit"; import IssueEdit from "@/business/issue/IssueEdit";
import IssueSyncSelect from "@/business/issue/IssueSyncSelect";
import { import {
checkSyncIssues, checkSyncIssues,
getIssuePartTemplateWithProject, getIssuePartTemplateWithProject,
@ -218,6 +220,7 @@ export default {
MsContainer, MsContainer,
IssueEdit, IssueEdit,
IssueDescriptionTableItem, IssueDescriptionTableItem,
IssueSyncSelect,
MsTableHeader, MsTableHeader,
MsTablePagination, MsTableButton, MsTableOperators, MsTableColumn, MsTable MsTablePagination, MsTableButton, MsTableOperators, MsTableColumn, MsTable
}, },
@ -251,6 +254,7 @@ export default {
userFilter: [], userFilter: [],
isThirdPart: false, isThirdPart: false,
creatorFilters: [], creatorFilters: [],
loading: false
}; };
}, },
watch: { watch: {
@ -406,14 +410,22 @@ export default {
return false; return false;
}, },
syncIssues() { syncIssues() {
this.page.result.loading = true; this.$refs.issueSyncSelect.open();
syncIssues() },
syncConfirm(data) {
this.loading = true;
let param = {
"projectId": getCurrentProjectID(),
"createTime": data.createTime.getTime(),
"pre": data.preValue
}
syncIssues(param)
.then((response) => { .then((response) => {
if (response.data === false) { if (response.data === false) {
checkSyncIssues(this.page.result); checkSyncIssues(this.loading);
} else { } else {
this.$success(this.$t('test_track.issue.sync_complete')); this.$success(this.$t('test_track.issue.sync_complete'));
this.page.result.loading = false; this.loading = false;
this.getIssues(); this.getIssues();
} }
}); });

View File

@ -0,0 +1,100 @@
<template>
<el-dialog :visible="visible" :title="$t('test_track.issue.sync_bugs')" @close="cancel">
<el-form ref="form" :model="form" :rules="rules" label-width="300px" :inline="true" size="small">
<el-form-item prop="typeValue">
<el-select v-model="form.typeValue">
<el-option
v-for="item in form.timeTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="preValue">
<el-select v-model="form.preValue">
<el-option
v-for="item in form.preOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="createTime">
<el-date-picker
v-model="form.createTime"
type="datetime"
placeholder="选择日期时间">
</el-date-picker>
</el-form-item>
<el-form-item class="tips-item">
<span class="tips">{{ $t('custom_field.sync_issue_tips') }}</span>
</el-form-item>
<el-form-item class="btn-group">
<el-button size="small" @click="cancel">{{ $t('commons.cancel') }}</el-button>
<el-button type="primary" size="small" @click="save">{{ $t('commons.confirm') }}</el-button>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
export default {
name: "IssueSyncSelect",
data() {
return {
visible: false,
form: {
timeTypeOptions: [{"value": "createTime", "label": "创建时间"}],
typeValue: "createTime",
preOptions: [{"value": false, "label": "大于等于"}, {"value": true, "label": "小于等于"}],
preValue: false,
createTime: ""
},
rules: {
createTime: [
{ type: 'date', required: true, message: "请选择日期时间", trigger: 'change'}
]
}
}
},
methods: {
open() {
this.visible = true;
},
save() {
this.$refs['form'].validate((valid) => {
if (valid) {
this.visible = false;
this.$emit('syncConfirm', this.form);
} else {
return false;
}
});
},
cancel() {
this.visible = false;
this.$refs.form.resetFields();
}
}
};
</script>
<style scoped>
.tips{
font-size: 13px;
}
.el-form-item.tips-item.el-form-item--small {
position: relative;
top: -20px;
left: 10px;
}
.el-form-item.btn-group.el-form-item--small {
float: right;
position: relative;
top: 25px;
}
</style>