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',
option_check: 'Please add 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: {
id: 'Workspace ID',

View File

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

View File

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

View File

@ -1,19 +1,17 @@
package io.metersphere.xpack.track.controller;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.xpack.track.dto.IssueSyncRequest;
import io.metersphere.xpack.track.service.XpackIssueService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/xpack/issue")
public class XpackIssueController {
@GetMapping("/sync/{projectId}")
public boolean getPlatformIssue(@PathVariable String projectId) {
@PostMapping("/sync")
public boolean getPlatformIssue(@RequestBody IssueSyncRequest request) {
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
*/
private String devopsFields;
private String azureDevopsCreateTime;
private PlatformStatusDTO transitions;

View File

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

View File

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

View File

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

View File

@ -136,9 +136,9 @@ public class IssuesController {
return issuesService.getZentaoBuilds(request);
}
@GetMapping("/sync/{projectId}")
public boolean getPlatformIssue(@PathVariable String projectId) {
return issuesService.syncThirdPartyIssues(projectId);
@PostMapping("/sync")
public boolean getPlatformIssue(@RequestBody IssueSyncRequest request) {
return issuesService.syncThirdPartyIssues(request);
}
@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.utils.DistinctKeyUtil;
import io.metersphere.xpack.track.dto.AttachmentSyncType;
import io.metersphere.xpack.track.dto.*;
import io.metersphere.constants.AttachmentType;
import io.metersphere.constants.SystemCustomField;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.xpack.track.dto.IssueTemplateDao;
import io.metersphere.i18n.Translator;
import io.metersphere.log.utils.ReflexObjectUtil;
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.testcase.AuthUserIssueRequest;
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.zentao.ZentaoBuild;
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.IssuesUpdateRequest;
import io.metersphere.service.issue.platform.*;
import io.metersphere.service.remote.project.TrackCustomFieldTemplateService;
import io.metersphere.service.remote.project.TrackIssueTemplateService;
import io.metersphere.service.wapper.TrackProjectService;
import io.metersphere.xpack.track.dto.PlatformStatusDTO;
import io.metersphere.xpack.track.issue.IssuesPlatform;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
@ -655,7 +651,9 @@ public class IssuesService {
List<String> projectIds = trackProjectService.getThirdPartProjectIds();
projectIds.forEach(id -> {
try {
syncThirdPartyIssues(id);
IssueSyncRequest request = new IssueSyncRequest();
request.setProjectId(id);
syncThirdPartyIssues(request);
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
}
@ -698,29 +696,32 @@ public class IssuesService {
stringRedisTemplate.delete(SYNC_THIRD_PARTY_ISSUES_KEY + ":" + projectId);
}
public boolean syncThirdPartyIssues(String projectId) {
if (StringUtils.isNotBlank(projectId)) {
String syncValue = getSyncKey(projectId);
public boolean syncThirdPartyIssues(IssueSyncRequest syncRequest) {
if (StringUtils.isNotBlank(syncRequest.getProjectId())) {
String syncValue = getSyncKey(syncRequest.getProjectId());
if (StringUtils.isNotEmpty(syncValue)) {
return false;
}
setSyncKey(projectId);
setSyncKey(syncRequest.getProjectId());
Project project = baseProjectService.getProjectById(projectId);
List<IssuesDao> issues = extIssuesMapper.getIssueForSync(projectId, project.getPlatform());
Project project = baseProjectService.getProjectById(syncRequest.getProjectId());
List<IssuesDao> issues = extIssuesMapper.getIssueForSync(syncRequest.getProjectId(), project.getPlatform());
if (syncRequest.getCreateTime() != null) {
issues = filterSyncIssuesByCreated(issues, syncRequest);
}
if (CollectionUtils.isEmpty(issues)) {
return true;
}
IssuesRequest issuesRequest = new IssuesRequest();
issuesRequest.setProjectId(projectId);
issuesRequest.setProjectId(syncRequest.getProjectId());
issuesRequest.setWorkspaceId(project.getWorkspaceId());
try {
if (!trackProjectService.isThirdPartTemplate(project)) {
String defaultCustomFields = getDefaultCustomFields(projectId);
String defaultCustomFields = getDefaultCustomFields(syncRequest.getProjectId());
issuesRequest.setDefaultCustomFields(defaultCustomFields);
}
IssuesPlatform platform = IssueFactory.createPlatform(project.getPlatform(), issuesRequest);
@ -728,7 +729,7 @@ public class IssuesService {
} catch (Exception e) {
throw e;
} finally {
deleteSyncKey(projectId);
deleteSyncKey(syncRequest.getProjectId());
}
}
return true;
@ -817,7 +818,6 @@ public class IssuesService {
public void calculatePlanReport(String planId, TestPlanSimpleReportDTO report) {
List<PlanReportIssueDTO> planReportIssueDTOS = extIssuesMapper.selectForPlanReport(planId);
planReportIssueDTOS = DistinctKeyUtil.distinctByKey(planReportIssueDTOS, PlanReportIssueDTO::getId);
TestPlanFunctionResultReportDTO functionResult = report.getFunctionResult();
List<TestCaseReportStatusResultDTO> statusResult = new ArrayList<>();
Map<String, TestCaseReportStatusResultDTO> statusResultMap = new HashMap<>();
@ -1087,4 +1087,15 @@ public class IssuesService {
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.utils.*;
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.PlatformStatusDTO;
import io.metersphere.dto.UserDTO;
@ -486,7 +487,7 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
}
@Override
public void syncAllIssues(Project project) {}
public void syncAllIssues(Project project, IssueSyncRequest syncRequest) {}
@Override
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() {
let url = 'issues/sync/';
export function syncIssues(param) {
let url = 'issues/sync';
if (hasLicense()) {
url = 'xpack/issue/sync/';
url = 'xpack/issue/sync';
}
// 浏览器默认策略请求同一个url可能导致 stalled 时间过长加个uuid防止请求阻塞
url = url + getCurrentProjectID() + "?stamp=" + getUUID();
return get(url);
url = url + "?stamp=" + getUUID();
return post(url, param);
}
// 轮询同步状态
export function checkSyncIssues(result, isNotFirst) {
export function checkSyncIssues(loading, isNotFirst) {
let url = 'issues/sync/check/' + getCurrentProjectID() + "?stamp=" + getUUID();
return get(url)
.then((response) => {
if (response.data === false) {
if (result.loading === true) {
if (loading === true) {
if (!isNotFirst) {
// 第一次才提示
$warning(i18n.t('test_track.issue.issue_sync_tip'));
}
setTimeout(() => checkSyncIssues(result, true), 1000);
setTimeout(() => checkSyncIssues(loading, true), 1000);
}
} else {
if (result.loading === true) {
if (loading === true) {
$success(i18n.t('test_track.issue.sync_complete'));
result.loading = false;
loading = false;
}
}
});

View File

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