feat(项目管理): 项目设置同步需求

This commit is contained in:
guoyuqi 2024-08-23 16:50:45 +08:00 committed by Craftsman
parent c707a65f22
commit 0b80fab0a6
15 changed files with 304 additions and 18 deletions

View File

@ -37,6 +37,8 @@ public class ProjectApplicationType {
public enum CASE_RELATED_CONFIG {
CASE_RELATED,
CASE_ENABLE,
CRON_EXPRESSION,
SYNC_ENABLE,
}
public enum PLATFORM_DEMAND_CONFIG {

View File

@ -5,6 +5,6 @@ public enum ScheduleResourceType {
API_SCENARIO,
TEST_PLAN,
CLEAN_REPORT,
DEMAND_SYNC,
BUG_SYNC
}

View File

@ -0,0 +1,59 @@
package io.metersphere.functional.job;
import io.metersphere.functional.service.DemandSyncService;
import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.ServiceIntegration;
import io.metersphere.system.schedule.BaseScheduleJob;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.TriggerKey;
/**
* 需求同步定时任务
*/
public class DemandSyncJob extends BaseScheduleJob {
private final DemandSyncService demandSyncService;
private final ProjectApplicationService projectApplicationService;
public DemandSyncJob() {
demandSyncService = CommonBeanFactory.getBean(DemandSyncService.class);
projectApplicationService = CommonBeanFactory.getBean(ProjectApplicationService.class);
}
public static JobKey getJobKey(String resourceId) {
return new JobKey(resourceId, DemandSyncJob.class.getName());
}
public static TriggerKey getTriggerKey(String resourceId) {
return new TriggerKey(resourceId, DemandSyncJob.class.getName());
}
@Override
protected void businessExecute(JobExecutionContext context) {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String resourceId = jobDataMap.getString("resourceId");
String userId = jobDataMap.getString("userId");
if (!checkBeforeSync(resourceId)) {
return;
}
LogUtils.info("Start synchronizing demands");
try{
demandSyncService.syncPlatformDemandBySchedule(resourceId, userId);
} catch (Exception e) {
LogUtils.error(e.getMessage());
}
}
/**
* 同步前检验, 同步配置的平台是否开启插件集成
* @return 是否放行
*/
private boolean checkBeforeSync(String projectId) {
ServiceIntegration serviceIntegration = projectApplicationService.getPlatformServiceIntegrationWithSyncOrDemand(projectId, true);
return serviceIntegration != null && serviceIntegration.getEnable();
}
}

View File

@ -1,5 +1,6 @@
package io.metersphere.functional.mapper;
import io.metersphere.functional.domain.FunctionalCaseDemand;
import io.metersphere.functional.dto.FunctionalDemandDTO;
import org.apache.ibatis.annotations.Param;
@ -12,4 +13,6 @@ public interface ExtFunctionalCaseDemandMapper {
List<FunctionalDemandDTO> selectParentDemandByKeyword(@Param("keyword") String keyword, @Param("caseId") String caseId);
List<FunctionalCaseDemand> selectDemandByProjectId(@Param("projectId") String projectId, @Param("platform") String platform);
}

View File

@ -17,4 +17,11 @@
</if>
</select>
<select id="selectDemandByProjectId" resultType="io.metersphere.functional.domain.FunctionalCaseDemand">
select functional_case_demand.id, functional_case_demand.demand_id, functional_case_demand.case_id
from functional_case_demand
left join functional_case on functional_case.id = functional_case_demand.case_id
where functional_case.project_id = #{caseId} and functional_case_demand.demand_platform=#{platform}
</select>
</mapper>

View File

@ -0,0 +1,70 @@
package io.metersphere.functional.service;
import io.metersphere.functional.job.DemandSyncJob;
import io.metersphere.project.domain.ProjectApplication;
import io.metersphere.sdk.constants.ProjectApplicationType;
import io.metersphere.sdk.constants.ScheduleResourceType;
import io.metersphere.sdk.constants.ScheduleType;
import io.metersphere.system.domain.Schedule;
import io.metersphere.system.schedule.ScheduleService;
import io.metersphere.system.service.BaseDemandScheduleService;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional(rollbackFor = Exception.class)
public class DemandScheduleServiceImpl implements BaseDemandScheduleService {
@Resource
private ScheduleService scheduleService;
@Override
public void updateDemandSyncScheduleConfig(List<ProjectApplication> bugSyncConfigs, String projectId, String currentUser) {
List<ProjectApplication> syncCron = bugSyncConfigs.stream().filter(config -> config.getType().equals(ProjectApplicationType.CASE_RELATED_CONFIG.CASE_RELATED.name() + "_" + ProjectApplicationType.CASE_RELATED_CONFIG.CRON_EXPRESSION.name())).toList();
List<ProjectApplication> caseEnable = bugSyncConfigs.stream().filter(config -> config.getType().equals(ProjectApplicationType.CASE_RELATED_CONFIG.CASE_RELATED.name() + "_" + ProjectApplicationType.CASE_RELATED_CONFIG.CASE_ENABLE.name())).toList();
List<ProjectApplication> syncEnable = bugSyncConfigs.stream().filter(config -> config.getType().equals(ProjectApplicationType.CASE_RELATED_CONFIG.CASE_RELATED.name() + "_" + ProjectApplicationType.CASE_RELATED_CONFIG.SYNC_ENABLE.name())).toList();
if (CollectionUtils.isNotEmpty(syncCron)) {
Boolean enableCase = Boolean.valueOf(caseEnable.getFirst().getTypeValue());
Boolean enableSync = Boolean.valueOf(syncEnable.getFirst().getTypeValue());
Boolean enable = enableCase && enableSync;
String typeValue = syncCron.getFirst().getTypeValue();
Schedule schedule = scheduleService.getScheduleByResource(projectId, DemandSyncJob.class.getName());
Optional<Schedule> optional = Optional.ofNullable(schedule);
optional.ifPresentOrElse(s -> {
s.setEnable(enable);
s.setValue(typeValue);
scheduleService.editSchedule(s);
scheduleService.addOrUpdateCronJob(s, DemandSyncJob.getJobKey(projectId), DemandSyncJob.getTriggerKey(projectId), DemandSyncJob.class);
}, () -> {
Schedule request = new Schedule();
request.setName("Demand Sync Job");
request.setResourceId(projectId);
request.setKey(IDGenerator.nextStr());
request.setProjectId(projectId);
request.setEnable(enable);
request.setCreateUser(currentUser);
request.setType(ScheduleType.CRON.name());
request.setValue(typeValue);
request.setJob(DemandSyncJob.class.getName());
request.setResourceType(ScheduleResourceType.DEMAND_SYNC.name());
scheduleService.addSchedule(request);
scheduleService.addOrUpdateCronJob(request, DemandSyncJob.getJobKey(projectId), DemandSyncJob.getTriggerKey(projectId), DemandSyncJob.class);
});
}
}
@Override
public void enableOrNotDemandSyncSchedule(String projectId, String currentUser, Boolean enable) {
Schedule schedule = scheduleService.getScheduleByResource(projectId, DemandSyncJob.class.getName());
if (schedule != null) {
schedule.setEnable(enable);
scheduleService.editSchedule(schedule);
}
}
}

View File

@ -0,0 +1,69 @@
package io.metersphere.functional.service;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.functional.domain.FunctionalCaseDemand;
import io.metersphere.functional.mapper.ExtFunctionalCaseDemandMapper;
import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.utils.PageUtils;
import io.metersphere.system.utils.Pager;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author guoyuqi
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class DemandSyncService {
@Resource
private ExtFunctionalCaseDemandMapper extFunctionalCaseDemandMapper;
@Resource
private ProjectApplicationService projectApplicationService;
public static int DEFAULT_BATCH_SIZE = 50;
/**
* 定时任务同步缺陷(存量-默认中文环境通知)
* @param projectId 项目ID
* @param scheduleUser 任务触发用户
*/
public void syncPlatformDemandBySchedule(String projectId, String scheduleUser) {
String platformName = projectApplicationService.getPlatformName(projectId);
// 创建一个 List 来保存合并后的结果
Map<String, List<FunctionalCaseDemand>> updateMap = new HashMap<>();
int pageNumber = 1;
boolean count = true;
Page<Object> page = PageHelper.startPage(pageNumber, DEFAULT_BATCH_SIZE, count);
Pager<List<FunctionalCaseDemand>> listPager = PageUtils.setPageInfo(page, extFunctionalCaseDemandMapper.selectDemandByProjectId(projectId,platformName));
long total = listPager.getTotal();
List<FunctionalCaseDemand> list = listPager.getList();
Map<String, List<FunctionalCaseDemand>> demandMap = list.stream().collect(Collectors.groupingBy(FunctionalCaseDemand::getDemandId));
Set<String> demandIds = demandMap.keySet();
//TODO: 调用三方接口获取最新需求, updateMap 缓存数据
count = false;
for (int i = 1; i < ((int)Math.ceil((double) total/DEFAULT_BATCH_SIZE)); i ++) {
Page<Object> pageCycle = PageHelper.startPage(i+1, DEFAULT_BATCH_SIZE, count);
Pager<List<FunctionalCaseDemand>> listPagerCycle = PageUtils.setPageInfo(pageCycle, extFunctionalCaseDemandMapper.selectDemandByProjectId(projectId,platformName));
List<FunctionalCaseDemand> pageResults = listPagerCycle.getList();
Map<String, List<FunctionalCaseDemand>> demandsMap = pageResults.stream().collect(Collectors.groupingBy(FunctionalCaseDemand::getDemandId));
Set<String> demandIdSet = demandsMap.keySet();
//TODO: 调用三方接口获取最新需求, updateMap 缓存数据
}
//TODO: 循环updateMap更新需求
LogUtils.info("End synchronizing demands");
}
}

View File

@ -5,7 +5,6 @@ import io.metersphere.project.dto.ModuleDTO;
import io.metersphere.project.request.ProjectApplicationRequest;
import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.project.service.ProjectService;
import io.metersphere.sdk.constants.ApplicationScope;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.ProjectApplicationType;
import io.metersphere.sdk.exception.MSException;
@ -25,7 +24,6 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -153,7 +151,7 @@ public class ProjectApplicationController {
@RequiresPermissions(PermissionConstants.PROJECT_APPLICATION_CASE_UPDATE)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.updateRelatedRequirementsLog(#projectId, #configs)", msClass = ProjectApplicationService.class)
public void updateRelated(@PathVariable("projectId") String projectId, @RequestBody Map<String, String> configs) {
projectApplicationService.updateRelated(projectId, configs);
projectApplicationService.updateRelated(projectId, configs, SessionUtils.getUserId());
}
@GetMapping("/case/related/info/{projectId}")

View File

@ -26,10 +26,7 @@ import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.log.dto.LogDTO;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.TestResourcePoolMapper;
import io.metersphere.system.service.BaseBugScheduleService;
import io.metersphere.system.service.PlatformPluginService;
import io.metersphere.system.service.PluginLoadService;
import io.metersphere.system.service.ServiceIntegrationService;
import io.metersphere.system.service.*;
import io.metersphere.system.utils.ServiceUtils;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
@ -52,9 +49,6 @@ public class ProjectApplicationService {
@Resource
private ExtProjectUserRoleMapper extProjectUserRoleMapper;
@Resource
private ProjectTestResourcePoolMapper projectTestResourcePoolMapper;
@Resource
private PluginMapper pluginMapper;
@ -110,6 +104,12 @@ public class ProjectApplicationService {
// 缺陷同步配置开启或关闭
baseBugScheduleService.enableOrNotBugSyncSchedule(application.getProjectId(), currentUser, Boolean.valueOf(application.getTypeValue()));
}
BaseDemandScheduleService baseDemandScheduleService = CommonBeanFactory.getBean(BaseDemandScheduleService.class);
if (StringUtils.equals(ProjectApplicationType.CASE_RELATED_CONFIG.CASE_RELATED.name() + "_" + ProjectApplicationType.CASE_RELATED_CONFIG.CASE_ENABLE.name(),
application.getType()) && baseDemandScheduleService != null && !Boolean.parseBoolean(application.getTypeValue())) {
// 需求同步配置关闭
baseDemandScheduleService.enableOrNotDemandSyncSchedule(application.getProjectId(), currentUser, Boolean.valueOf(application.getTypeValue()));
}
}
/**
@ -440,10 +440,10 @@ public class ProjectApplicationService {
/**
* 用例关联需求配置
*
* @param projectId
* @param configs
* @param projectId 项目ID
* @param configs 关联需求配置信息
*/
public void updateRelated(String projectId, Map<String, String> configs) {
public void updateRelated(String projectId, Map<String, String> configs, String currentUser) {
List<ProjectApplication> relatedConfigs = configs.entrySet().stream().map(config -> new ProjectApplication(projectId, ProjectApplicationType.CASE_RELATED_CONFIG.CASE_RELATED.name() + "_" + config.getKey().toUpperCase(), config.getValue())).collect(Collectors.toList());
ProjectApplicationExample example = new ProjectApplicationExample();
example.createCriteria().andProjectIdEqualTo(projectId).andTypeLike(ProjectApplicationType.CASE_RELATED_CONFIG.CASE_RELATED.name() + "%");
@ -455,6 +455,11 @@ public class ProjectApplicationService {
} else {
projectApplicationMapper.batchInsert(relatedConfigs);
}
// 更新需求定时任务配置
BaseDemandScheduleService baseDemandScheduleService = CommonBeanFactory.getBean(BaseDemandScheduleService.class);
if (baseDemandScheduleService != null) {
baseDemandScheduleService.updateDemandSyncScheduleConfig(relatedConfigs, projectId, currentUser);
}
}
/**

View File

@ -0,0 +1,24 @@
package io.metersphere.system.service;
import io.metersphere.project.domain.ProjectApplication;
import java.util.List;
public interface BaseDemandScheduleService {
/**
* 更新项目的需求同步定时任务
* @param demandSyncConfigs 配置
* @param projectId 项目ID
* @param currentUser 当前用户
*/
void updateDemandSyncScheduleConfig(List<ProjectApplication> demandSyncConfigs, String projectId, String currentUser);
/**
* 启用或禁用需求同步定时任务
* @param projectId 项目ID
* @param currentUser 当前用户
* @param enable 开启或禁用
*/
void enableOrNotDemandSyncSchedule(String projectId, String currentUser, Boolean enable);
}

View File

@ -12,7 +12,7 @@ public class SubSearchListUtil {
public static int DEFAULT_BATCH_SIZE = 50;
public <T> List<T> doForSearchList(Function<Object, List<T>> selectListFunc){
public static <T> List<T> doForSearchList(Function<Object, List<T>> selectListFunc){
// 创建一个 List 来保存合并后的结果
List<T> allResults = new ArrayList<>();
int pageNumber = 1;
@ -31,7 +31,7 @@ public class SubSearchListUtil {
return allResults;
}
public <T, V> List<T> doForSearchByCountList(List<V> totalList, Function<Object, List<T>> selectInListFunc){
public static <T, V> List<T> doForSearchByCountList(List<V> totalList, Function<Object, List<T>> selectInListFunc){
if (CollectionUtils.isEmpty(totalList)) {
return new ArrayList<>();
}

View File

@ -176,7 +176,13 @@ export function getBugSyncInfo(projectId: string) {
// 用例管理-获取关联需求信息
export function getCaseRelatedInfo(projectId: string) {
return MSR.get<{ demand_platform_config: string; platform_key: string; case_enable: string }>({
return MSR.get<{
demand_platform_config: string;
platform_key: string;
case_enable: string;
sync_enable: string;
cron_expression: string;
}>({
url: `${Url.getCaseRelatedInfoUrl}${projectId}`,
});
}

View File

@ -35,6 +35,39 @@
:form-rule="platformRules"
@mounted="handleMounted"
/>
<a-form-item>
<a-tooltip v-if="okDisabled" :content="t('project.menu.defect.enableAfterConfig')">
<a-switch size="small" type="line" disabled />
</a-tooltip>
<a-switch
v-else
v-model="form.SYNC_ENABLE"
checked-value="true"
unchecked-value="false"
size="small"
type="line"
/>
<span class="ml-[8px] text-[var(--color-text-1)]">
{{ t('project.menu.updateSync') }}
</span>
<a-tooltip position="tl" :content-style="{ maxWidth: '500px' }">
<template #content>
<div class="flex flex-col">
<div>{{ t('project.menu.updateSyncTip') }}</div>
</div>
</template>
<div>
<MsIcon
class="ml-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
type="icon-icon-maybe_outlined"
/>
</div>
</a-tooltip>
</a-form-item>
<!-- 同步频率 -->
<a-form-item field="CRON_EXPRESSION" :label="t('project.menu.CRON_EXPRESSION')">
<MsCronSelect v-model:model-value="form.CRON_EXPRESSION" />
</a-form-item>
</a-form>
<template v-if="platformOption.length" #footerLeft>
<div class="flex flex-row items-center gap-[4px]">
@ -74,6 +107,7 @@
<script lang="ts" setup>
import { FormInstance, Message } from '@arco-design/web-vue';
import MsCronSelect from '@/components/pure/ms-cron-select/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/ms-form-create.vue';
import type { FormItem, FormRuleItem } from '@/components/pure/ms-form-create/types';
@ -110,7 +144,9 @@
const form = reactive({
PLATFORM_KEY: '',
CASE_ENABLE: 'false', //
CASE_ENABLE: 'false', //
SYNC_ENABLE: 'false', //
CRON_EXPRESSION: '0 0 0 * * ?', //
});
const formCreateValue = ref<Record<string, any>>({});
@ -199,6 +235,8 @@
if (res && res.platform_key) {
form.CASE_ENABLE = res.case_enable;
form.PLATFORM_KEY = res.platform_key;
form.SYNC_ENABLE = res.sync_enable;
form.CRON_EXPRESSION = res.cron_expression;
formCreateValue.value = JSON.parse(res.demand_platform_config);
// keychange
await handlePlatformChange(res.platform_key);

View File

@ -48,6 +48,9 @@ export default {
'project.menu.notConfig': 'Third-party information not configured, click',
'project.menu.configure': 'to configure',
'project.menu.status': 'Status',
'project.menu.updateSync': 'Update mechanism',
'project.menu.updateSyncTip':
'When turned on, the associated requirements will be updated according to the set frequency.',
'project.menu.incrementalSync': 'Incremental Synchronization',
'project.menu.incrementalSyncTip': 'Only make content changes to existing third-party defects in MS',
'project.menu.fullSync': 'Full Synchronization',

View File

@ -44,6 +44,8 @@ export default {
'project.menu.notConfig': '未配置第三方信息,点击',
'project.menu.configure': '进行配置',
'project.menu.status': '状态',
'project.menu.updateSync': '更新机制',
'project.menu.updateSyncTip': '开启后,会根据设置的频率,更新已关联的需求',
'project.menu.incrementalSync': '增量同步',
'project.menu.incrementalSyncTip': '仅对 MS 中存在的三方缺陷做内容变更',
'project.menu.fullSync': '全量同步',