feat(缺陷管理): 补充关联接口场景用例功能

This commit is contained in:
song-cc-rock 2024-04-26 10:27:52 +08:00 committed by 刘瑞斌
parent 439742a2c5
commit 1598b5facb
23 changed files with 542 additions and 118 deletions

View File

@ -0,0 +1,29 @@
package io.metersphere.context;
import io.metersphere.provider.BaseAssociateCaseProvider;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 关联用例接口Bean实例上下文
*/
@Component
public class AssociateCaseFactory implements ApplicationContextAware {
public static final Map<String, BaseAssociateCaseProvider> PROVIDER_MAP = new HashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, BaseAssociateCaseProvider> beanMap = applicationContext.getBeansOfType(BaseAssociateCaseProvider.class);
PROVIDER_MAP.putAll(beanMap);
}
public static BaseAssociateCaseProvider getInstance(String serviceType) {
return PROVIDER_MAP.get(serviceType);
}
}

View File

@ -6,6 +6,9 @@ import io.metersphere.request.TestCasePageProviderRequest;
import java.util.List;
/**
* 多个实现(关联用例基础接口)
*/
public interface BaseAssociateCaseProvider {
/**

View File

@ -1,6 +1,5 @@
package io.metersphere.sdk.constants;
import io.metersphere.sdk.util.Translator;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
@ -10,41 +9,41 @@ public enum CaseType {
/**
* 功能用例
*/
FUNCTIONAL_CASE("FUNCTIONAL", "test_case"),
FUNCTIONAL_CASE("FUNCTIONAL", "test_case", PermissionConstants.FUNCTIONAL_CASE_READ, "functional_case", "functional_case_module", "functional_case.module.default.name"),
/**
* 接口用例
*/
API_CASE("API", "api_case"),
API_CASE("API", "api_case", PermissionConstants.PROJECT_API_DEFINITION_CASE_READ, "api_test_case", "api_definition_module", "api_unplanned_request"),
/**
* 性能用例
* 场景用例
*/
SCENARIO_CASE("SCENARIO", "scenario_case"),
/**
* UI用例
*/
UI_CASE("UI", "ui_case"),
/**
* 性能用例
*/
PERFORMANCE_CASE("PERFORMANCE", "performance_case");
SCENARIO_CASE("SCENARIO", "scenario_case", PermissionConstants.PROJECT_API_SCENARIO_READ, "api_scenario", "api_scenario_module", "api_unplanned_scenario");
private final String key;
private final String value;
private final String type;
CaseType(String key, String value) {
private final String usePermission;
private final String caseTable;
private final String moduleTable;
private final String unPlanName;
CaseType(String key, String type, String usePermission, String caseTable, String moduleTable, String unPlanName) {
this.key = key;
this.value = value;
this.type = type;
this.usePermission = usePermission;
this.caseTable = caseTable;
this.moduleTable = moduleTable;
this.unPlanName = unPlanName;
}
public String getValue() {
return Translator.get(value);
}
public static String getValue(String key) {
public static CaseType getType(String key) {
for (CaseType caseType : CaseType.values()) {
if (StringUtils.equals(caseType.getKey(), key)) {
return caseType.getValue();
return caseType;
}
}
return null;

View File

@ -94,6 +94,7 @@ bug_comment_not_exist=缺陷评论不存在
bug_comment_not_owner=非当前评论创建人, 无法操作!
bug_relate_case_not_found=未查询到关联的用例
bug_relate_case_type_unknown=关联的用例类型未知, 无法查看
unknown_case_type_of_relate_case=参数错误, 未知的用例类型
bug_relate_case_permission_error=无用例查看权限, 请联系管理员
bug_status_can_not_be_empty=缺陷状态不能为空
handle_user_can_not_be_empty=缺陷处理人不能为空

View File

@ -94,6 +94,7 @@ bug_comment_not_exist=Bug comment does not exist
bug_comment_not_owner=Not owner of the bug comment!
bug_relate_case_not_found=Bug related case not found
bug_relate_case_type_unknown=Bug related case type unknown
unknown_case_type_of_relate_case=Parameter error, unknown case type
bug_relate_case_permission_error=No permission to show the case
bug_status_can_not_be_empty=Status cannot be empty
handle_user_can_not_be_empty=Handle user cannot be empty

View File

@ -94,6 +94,7 @@ bug_comment_not_exist=缺陷评论不存在
bug_comment_not_owner=非当前评论创建人, 无法操作!
bug_relate_case_not_found=未查询到关联的用例
bug_relate_case_type_unknown=关联的用例类型未知, 无法查看
unknown_case_type_of_relate_case=参数错误, 未知的用例类型
bug_relate_case_permission_error=无用例查看权限, 请联系管理员
bug_status_can_not_be_empty=缺陷状态不能为空
handle_user_can_not_be_empty=缺陷处理人不能为空

View File

@ -94,6 +94,7 @@ bug_comment_not_exist=缺陷評論不存在
bug_comment_not_owner=非當前評論創建人, 無法操作!
bug_relate_case_not_found=未查詢到關聯的用例
bug_relate_case_type_unknown=關聯的用例類型未知, 無法查看
unknown_case_type_of_relate_case=參數錯誤, 未知的用例類型
bug_relate_case_permission_error=無權限查看, 請聯繫管理員
bug_status_can_not_be_empty=缺陷狀態不能為空
handle_user_can_not_be_empty=缺陷處理人不能為空

View File

@ -59,4 +59,21 @@ public interface ExtApiScenarioMapper {
List<ApiScenario> getScenarioExecuteInfoByIds(@Param("ids") List<String> ids);
List<ModuleCountDTO> countModuleIdByRequest(@Param("request") ApiScenarioModuleRequest request, @Param("deleted") boolean deleted);
/**
* 获取缺陷未关联的场景用例列表
* @param request provider参数
* @param deleted 是否删除状态
* @param sort 排序
* @return 通用的列表Case集合
*/
List<TestCaseProviderDTO> listUnRelatedCaseWithBug(@Param("request") TestCasePageProviderRequest request, @Param("deleted") boolean deleted, @Param("sort") String sort);
/**
* 根据关联条件获取关联的用例ID
* @param request 关联参数
* @param deleted 是否删除状态
* @return 关联的用例ID集合
*/
List<String> getSelectIdsByAssociateParam(@Param("request")AssociateOtherCaseRequest request, @Param("deleted") boolean deleted);
}

View File

@ -555,6 +555,49 @@
GROUP BY api_scenario.module_id
</select>
<select id="listUnRelatedCaseWithBug" resultMap="TestCaseProviderDTO">
select
ao.id,
ao.num,
ao.name,
ao.priority,
ao.project_id,
ao.tags,
pv.name as versionName,
ao.create_user,
u.name as createUserName,
ao.create_time
from api_scenario ao
left join project_version pv ON ao.version_id = pv.id
left join user u ON ao.create_user = u.id
where ao.deleted = #{deleted}
and ao.project_id = #{request.projectId}
and ao.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryByTestCaseProviderParam"/>
order by
<if test="sort != null and sort != ''">
ao.${sort}
</if>
<if test="sort == null or sort == ''">
ao.create_time desc
</if>
</select>
<select id="getSelectIdsByAssociateParam" resultType="java.lang.String">
select ac.id
from api_test_case ac
where ac.deleted = #{deleted}
and ac.project_id = #{request.projectId}
and ac.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryByAssociateParam"/>
</select>
<sql id="report_filters">
<if test="${filter} != null and ${filter}.size() > 0">
<foreach collection="${filter}.entrySet()" index="key" item="values">
@ -577,4 +620,38 @@
</foreach>
</if>
</sql>
<sql id="queryByTestCaseProviderParam">
<!-- 待补充关联Case弹窗中的高级搜索条件filter, combine -->
<if test="request.keyword != null and request.keyword != ''">
and (
ao.num like concat('%', #{request.keyword}, '%')
or ao.name like concat('%', #{request.keyword}, '%')
or ao.tags like concat('%', #{request.keyword}, '%')
)
</if>
<if test="request.moduleIds != null and request.moduleIds.size() > 0">
and ao.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
</if>
</sql>
<sql id="queryByAssociateParam">
<!-- 待补充关联Case弹窗中的高级搜索条件filter, combine -->
<if test="request.condition.keyword != null and request.condition.keyword != ''">
and (
ao.num like concat('%', #{request.keyword}, '%')
or ao.name like concat('%', #{request.keyword}, '%')
or ao.tags like concat('%', #{request.keyword}, '%')
)
</if>
<if test="request.moduleIds != null and request.moduleIds.size() > 0">
and ao.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
</if>
</sql>
</mapper>

View File

@ -76,4 +76,21 @@ public interface ExtApiTestCaseMapper {
DropNode selectNodeByPosOperator(NodeSortQueryParam nodeSortQueryParam);
List<ApiTestCase> getApiCaseExecuteInfoByIds(@Param("ids")List<String> ids);
/**
* 获取缺陷未关联的接口用例列表
* @param request provider参数
* @param deleted 是否删除状态
* @param sort 排序
* @return 通用的列表Case集合
*/
List<TestCaseProviderDTO> listUnRelatedCaseWithBug(@Param("request") TestCasePageProviderRequest request, @Param("deleted") boolean deleted, @Param("sort") String sort);
/**
* 根据关联条件获取关联的用例ID
* @param request 关联参数
* @param deleted 是否删除状态
* @return 关联的用例ID集合
*/
List<String> getSelectIdsByAssociateParam(@Param("request")AssociateOtherCaseRequest request, @Param("deleted") boolean deleted);
}

View File

@ -336,6 +336,52 @@
#{id}
</foreach>
</select>
<select id="listUnRelatedCaseWithBug" resultMap="TestCaseProviderDTO">
select
ac.id,
ac.num,
ac.name,
ac.priority,
ac.project_id,
ac.tags,
pv.name as versionName,
ac.create_user,
u.name as createUserName,
ac.create_time
from api_test_case ac
inner join api_definition ad on ac.api_definition_id = ad.id
left join project_version pv ON ac.version_id = pv.id
left join user u ON ac.create_user = u.id
where ac.deleted = #{deleted}
and ac.project_id = #{request.projectId}
and ac.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryByTestCaseProviderParam"/>
order by
<if test="sort != null and sort != ''">
ac.${sort}
</if>
<if test="sort == null or sort == ''">
ac.create_time desc
</if>
</select>
<select id="getSelectIdsByAssociateParam" resultType="java.lang.String">
select ac.id
from api_test_case ac
inner join api_definition ad on ac.api_definition_id = ad.id
where ac.deleted = #{deleted}
and ac.project_id = #{request.projectId}
and ac.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryByAssociateParam"/>
</select>
<sql id="report_filters">
<if test="${filter} != null and ${filter}.size() > 0">
<foreach collection="${filter}.entrySet()" index="key" item="values">
@ -484,4 +530,50 @@
AND a.latest = 1
</if>
</sql>
<sql id="queryByTestCaseProviderParam">
<!-- 待补充关联Case弹窗中的高级搜索条件filter, combine -->
<if test="request.keyword != null and request.keyword != ''">
and (
ac.num like concat('%', #{request.keyword}, '%')
or ac.name like concat('%', #{request.keyword}, '%')
or ac.tags like concat('%', #{request.keyword}, '%')
)
</if>
<if test="request.moduleIds != null and request.moduleIds.size() > 0">
and ac.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
</if>
<if test="request.protocol != null and request.protocol!=''">
and ad.protocol = #{request.protocol}
</if>
<if test="request.apiDefinitionId != null and request.apiDefinitionId!=''">
and ac.api_definition_id = #{request.apiDefinitionId}
</if>
</sql>
<sql id="queryByAssociateParam">
<!-- 待补充关联Case弹窗中的高级搜索条件filter, combine -->
<if test="request.condition.keyword != null and request.condition.keyword != ''">
and (
ac.num like concat('%', #{request.keyword}, '%')
or ac.name like concat('%', #{request.keyword}, '%')
or ac.tags like concat('%', #{request.keyword}, '%')
)
</if>
<if test="request.moduleIds != null and request.moduleIds.size() > 0">
and ac.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
</if>
<if test="request.protocol != null and request.protocol!=''">
and ad.protocol = #{request.protocol}
</if>
<if test="request.apiDefinitionId != null and request.apiDefinitionId!=''">
and ac.api_definition_id = #{request.apiDefinitionId}
</if>
</sql>
</mapper>

View File

@ -8,6 +8,7 @@ import io.metersphere.api.service.definition.ApiDefinitionModuleService;
import io.metersphere.dto.TestCaseProviderDTO;
import io.metersphere.project.dto.ModuleCountDTO;
import io.metersphere.provider.BaseAssociateApiProvider;
import io.metersphere.provider.BaseAssociateCaseProvider;
import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest;
import io.metersphere.sdk.util.Translator;
@ -16,12 +17,13 @@ import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public class AssociateApiProvider implements BaseAssociateApiProvider {
@Service("API")
public class AssociateApiProvider implements BaseAssociateApiProvider, BaseAssociateCaseProvider {
@Resource
private ExtApiTestCaseMapper extApiTestCaseMapper;
@ -88,4 +90,26 @@ public class AssociateApiProvider implements BaseAssociateApiProvider {
return moduleTreeService.buildTreeAndCountResource(fileModuleList, moduleCountDTOList, true, Translator.get(UNPLANNED_API));
}
@Override
public List<TestCaseProviderDTO> listUnRelatedTestCaseList(TestCasePageProviderRequest request) {
List<TestCaseProviderDTO> apiCases = extApiTestCaseMapper.listUnRelatedCaseWithBug(request, false, request.getSortString());
if (CollectionUtils.isEmpty(apiCases)) {
return new ArrayList<>();
}
return apiCases;
}
@Override
public List<String> getRelatedIdsByParam(AssociateOtherCaseRequest request, boolean deleted) {
if (request.isSelectAll()) {
List<String> relatedIds = extApiTestCaseMapper.getSelectIdsByAssociateParam(request, deleted);
if (CollectionUtils.isNotEmpty(request.getExcludeIds())) {
relatedIds = relatedIds.stream().filter(id -> !request.getExcludeIds().contains(id)).toList();
}
return relatedIds;
} else {
return request.getSelectIds();
}
}
}

View File

@ -7,6 +7,7 @@ import io.metersphere.api.mapper.ExtApiScenarioMapper;
import io.metersphere.api.service.scenario.ApiScenarioModuleService;
import io.metersphere.dto.TestCaseProviderDTO;
import io.metersphere.project.dto.ModuleCountDTO;
import io.metersphere.provider.BaseAssociateCaseProvider;
import io.metersphere.provider.BaseAssociateScenarioProvider;
import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest;
@ -16,11 +17,12 @@ import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public class AssociateScenarioProvider implements BaseAssociateScenarioProvider {
@Service("SCENARIO")
public class AssociateScenarioProvider implements BaseAssociateScenarioProvider, BaseAssociateCaseProvider {
@Resource
private ExtApiScenarioMapper extApiScenarioMapper;
@ -87,4 +89,26 @@ public class AssociateScenarioProvider implements BaseAssociateScenarioProvider
List<BaseTreeNode> fileModuleList = extApiScenarioMapper.selectIdAndParentIdByProjectId(request.getProjectId());
return apiScenarioModuleService.buildTreeAndCountResource(fileModuleList, moduleCountDTOList, true, Translator.get(UNPLANNED_SCENARIO));
}
@Override
public List<TestCaseProviderDTO> listUnRelatedTestCaseList(TestCasePageProviderRequest request) {
List<TestCaseProviderDTO> apiScenarios = extApiScenarioMapper.listUnRelatedCaseWithBug(request, false, request.getSortString());
if (CollectionUtils.isEmpty(apiScenarios)) {
return new ArrayList<>();
}
return apiScenarios;
}
@Override
public List<String> getRelatedIdsByParam(AssociateOtherCaseRequest request, boolean deleted) {
if (request.isSelectAll()) {
List<String> relatedIds = extApiScenarioMapper.getSelectIdsByAssociateParam(request, deleted);
if (CollectionUtils.isNotEmpty(request.getExcludeIds())) {
relatedIds = relatedIds.stream().filter(id -> !request.getExcludeIds().contains(id)).toList();
}
return relatedIds;
} else {
return request.getSelectIds();
}
}
}

View File

@ -0,0 +1,76 @@
package io.metersphere.api.service;
import io.metersphere.api.provider.AssociateApiProvider;
import io.metersphere.api.provider.AssociateScenarioProvider;
import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest;
import io.metersphere.system.base.BaseTest;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import java.util.List;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AssociateCaseProviderTests extends BaseTest {
@Resource
AssociateApiProvider apiProvider;
@Resource
AssociateScenarioProvider scenarioProvider;
@Test
@Order(1)
@Sql(scripts = {"/dml/init_associate_provider_test.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED))
void coverApiProvider() {
TestCasePageProviderRequest request = new TestCasePageProviderRequest();
request.setProjectId("test-associate-pro");
request.setSourceId("test-source-id");
request.setSourceType("test-source-type");
apiProvider.listUnRelatedTestCaseList(request);
request.setKeyword("api-case-associate-2");
apiProvider.listUnRelatedTestCaseList(request);
AssociateOtherCaseRequest associateRequest = new AssociateOtherCaseRequest();
associateRequest.setSelectAll(true);
associateRequest.setProjectId("test-associate-pro");
associateRequest.setSourceId("test-source-id");
associateRequest.setSourceType("test-source-type");
apiProvider.getRelatedIdsByParam(associateRequest, false);
associateRequest.setExcludeIds(List.of("api-case-associate-1"));
apiProvider.getRelatedIdsByParam(associateRequest, false);
associateRequest.setSelectAll(false);
associateRequest.setSelectIds(List.of("api-case-associate-1"));
apiProvider.getRelatedIdsByParam(associateRequest, false);
}
@Test
@Order(2)
void coverScenarioProvider() {
TestCasePageProviderRequest request = new TestCasePageProviderRequest();
request.setProjectId("test-associate-pro");
request.setSourceId("test-source-id");
request.setSourceType("test-source-type");
scenarioProvider.listUnRelatedTestCaseList(request);
request.setKeyword("api-scenario-associate-2");
scenarioProvider.listUnRelatedTestCaseList(request);
AssociateOtherCaseRequest associateRequest = new AssociateOtherCaseRequest();
associateRequest.setSelectAll(true);
associateRequest.setProjectId("test-associate-pro");
associateRequest.setSourceId("test-source-id");
associateRequest.setSourceType("test-source-type");
scenarioProvider.getRelatedIdsByParam(associateRequest, false);
associateRequest.setExcludeIds(List.of("api-case-scenario-1"));
scenarioProvider.getRelatedIdsByParam(associateRequest, false);
associateRequest.setSelectAll(false);
associateRequest.setSelectIds(List.of("api-case-scenario-1"));
scenarioProvider.getRelatedIdsByParam(associateRequest, false);
}
}

View File

@ -0,0 +1,11 @@
INSERT INTO `api_definition` (`id`, `name`, `protocol`, `method`, `path`, `status`, `num`, `tags`, `pos`, `project_id`, `module_id`, `latest`, `version_id`, `ref_id`, `description`, `create_time`, `create_user`, `update_time`, `update_user`, `delete_user`, `delete_time`, `deleted`)
VALUES ('api-definition-tmp', 'test31931', 'HTTP', 'POST', '/api/test', 'PROCESSING', 1000001, '[\"test3\",\"te\"]', 1, 'test-associate-pro', 'root', b'1', '100570499574136985', '1001', NULL, UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', NULL, NULL, false);
INSERT INTO `api_definition` (`id`, `name`, `protocol`, `method`, `path`, `status`, `num`, `tags`, `pos`, `project_id`, `module_id`, `latest`, `version_id`, `ref_id`, `description`, `create_time`, `create_user`, `update_time`, `update_user`, `delete_user`, `delete_time`, `deleted`)
VALUES ('api-definition-associate-1', 'test31931313', 'HTTP', 'POST', '/api/test', 'PROCESSING', 1000001, '[\"test3\",\"te\"]', 1, 'test-associate-pro', 'root', b'1', '100570499574136985', '1001', NULL, UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', NULL, NULL, false);
INSERT INTO api_test_case(id, name, priority, num, tags, status, last_report_status, last_report_id, pos, project_id, api_definition_id, version_id, environment_id, create_time, create_user, update_time, update_user, delete_time, delete_user, deleted)
VALUES ('api-case-associate-1','test4938131', 'P0', 10000023131, null, 'Underway', 'PENDING', null, 1, 'test-associate-pro', 'api-definition-associate-1', '100570499574136985', 'test_associate_env_id', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', null, null, false);
INSERT INTO api_scenario(id, name, priority, status, last_report_status, last_report_id, num, pos, version_id, ref_id, project_id, module_id, description, tags, create_user, create_time, delete_time, delete_user, update_user, update_time, deleted)
VALUES ('api-scenario-associate-1', 'test323131', 'p1', 'test-api-status', 'PENDING', null, 1000001, 1, 'v1.10', 'api-scenario-associate-rid', 'test-associate-pro', 'root', null, null, 'admin', UNIX_TIMESTAMP() * 1000, null, null, 'admin', UNIX_TIMESTAMP() * 1000, false);

View File

@ -7,6 +7,7 @@ import io.metersphere.bug.dto.request.BugRelatedCasePageRequest;
import io.metersphere.bug.dto.response.BugRelateCaseDTO;
import io.metersphere.bug.service.BugRelateCaseCommonService;
import io.metersphere.bug.service.BugRelateCaseLogService;
import io.metersphere.context.AssociateCaseFactory;
import io.metersphere.dto.TestCaseProviderDTO;
import io.metersphere.provider.BaseAssociateCaseProvider;
import io.metersphere.request.AssociateCaseModuleRequest;
@ -37,23 +38,22 @@ public class BugRelateCaseController {
@Resource
private BugRelateCaseCommonService bugRelateCaseCommonService;
@Resource
private BaseAssociateCaseProvider functionalCaseProvider;
@PostMapping("/un-relate/page")
@Operation(description = "缺陷管理-关联用例-未关联用例-列表分页")
@RequiresPermissions(PermissionConstants.PROJECT_BUG_READ)
public Pager<List<TestCaseProviderDTO>> unRelatedPage(@Validated @RequestBody TestCasePageProviderRequest request) {
// 目前只保留功能用例的Provider接口, 后续其他用例根据RelateCaseType扩展
bugRelateCaseCommonService.checkCaseTypeParamIllegal(request.getSourceType());
BaseAssociateCaseProvider associateCaseProvider = AssociateCaseFactory.getInstance(request.getSourceType());
Page<Object> page = PageHelper.startPage(request.getCurrent(), request.getPageSize());
return PageUtils.setPageInfo(page, functionalCaseProvider.listUnRelatedTestCaseList(request));
return PageUtils.setPageInfo(page, associateCaseProvider.listUnRelatedTestCaseList(request));
}
@PostMapping("/un-relate/module/count")
@Operation(summary = "缺陷管理-关联用例-未关联用例-模块树数量")
@RequiresPermissions(PermissionConstants.PROJECT_BUG_READ)
@CheckOwner(resourceId = "#request.projectId", resourceType = "project")
public Map<String, Long> countTree(@RequestBody @Validated TestCasePageProviderRequest request) {
public Map<String, Long> countTree(@RequestBody @Validated AssociateCaseModuleRequest request) {
return bugRelateCaseCommonService.countTree(request);
}

View File

@ -7,7 +7,6 @@ import io.metersphere.dto.BugProviderDTO;
import io.metersphere.project.dto.ModuleCountDTO;
import io.metersphere.request.AssociateBugPageRequest;
import io.metersphere.request.AssociateCaseModuleRequest;
import io.metersphere.request.TestCasePageProviderRequest;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import org.apache.ibatis.annotations.Param;
@ -21,17 +20,20 @@ public interface ExtBugRelateCaseMapper {
/**
* 获取缺陷关联的用例模块树
* @param request 请求参数
* @param caseTable 关联用例表
* @param moduleTable 关联用例模块表
* @return 模块树集合
*/
List<BaseTreeNode> getRelateCaseModule(@Param("request") AssociateCaseModuleRequest request);
List<BaseTreeNode> getRelateCaseModule(@Param("request") AssociateCaseModuleRequest request, @Param("caseTable") String caseTable, @Param("moduleTable") String moduleTable);
/**
* 获取缺陷关联的用例模块树数量
* @param request 请求参数
* @param deleted 是否删除状态
* @param caseTable 关联用例表
* @return 模块树数量
*/
List<ModuleCountDTO> countRelateCaseModuleTree(@Param("request") TestCasePageProviderRequest request, @Param("deleted") boolean deleted);
List<ModuleCountDTO> countRelateCaseModuleTree(@Param("request") AssociateCaseModuleRequest request, @Param("deleted") boolean deleted, @Param("caseTable") String caseTable);
/**
* 统计缺陷关联的用例数量

View File

@ -3,34 +3,55 @@
<mapper namespace="io.metersphere.bug.mapper.ExtBugRelateCaseMapper">
<select id="getRelateCaseModule" resultType="io.metersphere.system.dto.sdk.BaseTreeNode">
select
distinct fcm.id,
fcm.parent_id as parentId,
fcm.name,
fcm.pos,
fcm.project_id
from functional_case_module fcm left join functional_case fc on fc.module_id = fcm.id
where fcm.project_id = #{request.projectId}
and fc.id not in
distinct mt.id,
mt.parent_id as parentId,
mt.name,
mt.pos,
mt.project_id
from ${moduleTable} mt
<if test="caseTable != null and caseTable != '' and caseTable != 'api_test_case'">
left join ${caseTable} ct on ct.module_id = mt.id
</if>
<if test="caseTable != null and caseTable != '' and caseTable == 'api_test_case'">
left join api_definition ad on ad.module_id = mt.id
left join api_test_case ct on ct.api_definition_id = ad.id
</if>
where mt.project_id = #{request.projectId}
and ct.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryModuleWhereCondition"/>
order by pos
order by mt.pos
</select>
<select id="countRelateCaseModuleTree" resultType="io.metersphere.project.dto.ModuleCountDTO">
select
fc.module_id AS moduleId,
count(fc.id) as dataCount
from functional_case fc
where fc.deleted = #{deleted}
and fc.project_id = #{request.projectId}
and fc.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryModuleWhereCondition"/>
group by fc.module_id
<if test="caseTable != null and caseTable != '' and caseTable == 'api_test_case'">
ad.module_id as moduleId,
count(ad.id) as dataCount
</if>
<if test="caseTable != null and caseTable != '' and caseTable != 'api_test_case'">
ct.module_id as moduleId,
count(ct.id) as dataCount
</if>
from ${caseTable} ct
<if test="caseTable != null and caseTable != '' and caseTable == 'api_test_case'">
join api_definition ad on ct.api_definition_id = ad.id
</if>
where ct.deleted = #{deleted}
and ct.project_id = #{request.projectId}
and ct.id not in
(
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
)
<include refid="queryModuleWhereCondition"/>
<if test="caseTable != null and caseTable != '' and caseTable == 'api_test_case'">
group by ad.module_id
</if>
<if test="caseTable != null and caseTable != '' and caseTable != 'api_test_case'">
group by ct.module_id
</if>
</select>
<select id="countRelationCases" resultType="io.metersphere.bug.dto.response.BugRelateCaseCountDTO">
@ -45,15 +66,22 @@
<select id="list" resultType="io.metersphere.bug.dto.response.BugRelateCaseDTO">
select brc.id relateId, fc.id relateCaseId, fc.num relateCaseNum, fc.name relateCaseName, fc.project_id projectId, fc.version_id versionId, brc.case_type relateCaseType,
brc.test_plan_id is not null relatePlanCase, brc.case_id is not null relateCase
from bug_relation_case brc join functional_case fc on (brc.case_id = fc.id or brc.test_plan_case_id = fc.id)
where brc.bug_id = #{request.bugId} and fc.deleted = false
from bug_relation_case brc
left join functional_case fc on ((brc.case_id = fc.id or brc.test_plan_case_id = fc.id) and fc.deleted = false)
left join api_test_case atc on ((brc.case_id = atc.id or brc.test_plan_case_id = atc.id) and atc.deleted = false)
left join api_scenario ao on ((brc.case_id = ao.id or brc.test_plan_case_id = ao.id) and ao.deleted = false)
where brc.bug_id = #{request.bugId}
<if test="request.keyword != null and request.keyword != ''">
and (
fc.name like concat('%', #{request.keyword}, '%')
or fc.num like concat('%', #{request.keyword}, '%')
fc.name like concat('%', #{request.keyword}, '%')
or fc.num like concat('%', #{request.keyword}, '%')
or atc.name like concat('%', #{request.keyword}, '%')
or atc.num like concat('%', #{request.keyword}, '%')
or ao.name like concat('%', #{request.keyword}, '%')
or ao.num like concat('%', #{request.keyword}, '%')
)
</if>
order by brc.id desc
order by brc.create_time desc
</select>
<select id="getRelateCase" resultType="io.metersphere.bug.dto.response.BugRelateCaseDTO">
@ -171,17 +199,27 @@
</sql>
<sql id="queryModuleWhereCondition">
<!-- 待补充关联Case弹窗中的高级搜索条件 -->
<!-- 待补充关联Case弹窗中的高级搜索条件filter, combine -->
<if test="request.keyword != null and request.keyword != ''">
and (
fc.num like concat('%', #{request.keyword}, '%') or fc.name like concat('%', #{request.keyword}, '%')
ct.num like concat('%', #{request.keyword}, '%')
or ct.name like concat('%', #{request.keyword}, '%')
or ct.tags like concat('%', #{request.keyword}, '%')
)
</if>
<if test="request.moduleIds != null and request.moduleIds.size() > 0">
and fc.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
<if test="caseTable != null and caseTable != '' and caseTable == 'api_test_case'">
and ad.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
</if>
<if test="caseTable != null and caseTable != '' and caseTable != 'api_test_case'">
and ct.module_id in
<foreach collection="request.moduleIds" item="moduleId" open="(" separator="," close=")">
#{moduleId}
</foreach>
</if>
</if>
</sql>
</mapper>

View File

@ -7,6 +7,7 @@ import io.metersphere.bug.dto.request.BugRelatedCasePageRequest;
import io.metersphere.bug.dto.response.BugRelateCaseDTO;
import io.metersphere.bug.mapper.BugRelationCaseMapper;
import io.metersphere.bug.mapper.ExtBugRelateCaseMapper;
import io.metersphere.context.AssociateCaseFactory;
import io.metersphere.project.domain.Project;
import io.metersphere.project.domain.ProjectExample;
import io.metersphere.project.domain.ProjectVersion;
@ -19,11 +20,8 @@ import io.metersphere.project.service.PermissionCheckService;
import io.metersphere.provider.BaseAssociateCaseProvider;
import io.metersphere.request.AssociateCaseModuleRequest;
import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest;
import io.metersphere.sdk.constants.CaseType;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.uid.IDGenerator;
@ -40,6 +38,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@ -58,8 +57,27 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
private ExtBugRelateCaseMapper extBugRelateCaseMapper;
@Resource
private PermissionCheckService permissionCheckService;
@Resource
private BaseAssociateCaseProvider functionalCaseProvider;
/**
* 获取关联用例模块树数量
* @param request 请求参数
* @return 模块树集合
*/
public Map<String, Long> countTree(AssociateCaseModuleRequest request) {
// 用例类型参数非法校验
this.checkCaseTypeParamIllegal(request.getSourceType());
// 统计模块数量不用传模块ID
request.setModuleIds(null);
List<ModuleCountDTO> moduleCounts = extBugRelateCaseMapper.countRelateCaseModuleTree(request, false, Objects.requireNonNull(CaseType.getType(request.getSourceType())).getCaseTable());
List<BaseTreeNode> relateCaseModules = extBugRelateCaseMapper.getRelateCaseModule(request,
Objects.requireNonNull(CaseType.getType(request.getSourceType())).getCaseTable(), Objects.requireNonNull(CaseType.getType(request.getSourceType())).getModuleTable());
List<BaseTreeNode> relateCaseModuleWithCount = buildTreeAndCountResource(relateCaseModules, moduleCounts, true,
Translator.get(Objects.requireNonNull(CaseType.getType(request.getSourceType())).getUnPlanName()));
Map<String, Long> moduleCountMap = getIdCountMapByBreadth(relateCaseModuleWithCount);
long total = getAllCount(moduleCounts);
moduleCountMap.put("total", total);
return moduleCountMap;
}
/**
* 获取关联用例模块树(不包括数量)
@ -67,30 +85,12 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
* @return 模块树集合
*/
public List<BaseTreeNode> getRelateCaseTree(AssociateCaseModuleRequest request) {
// 目前只保留功能用例的左侧模块树方法调用, 后续其他用例根据RelateCaseType扩展
List<BaseTreeNode> relateCaseModules = extBugRelateCaseMapper.getRelateCaseModule(request);
// 用例类型参数非法校验
this.checkCaseTypeParamIllegal(request.getSourceType());
List<BaseTreeNode> relateCaseModules = extBugRelateCaseMapper.getRelateCaseModule(request,
Objects.requireNonNull(CaseType.getType(request.getSourceType())).getCaseTable(), Objects.requireNonNull(CaseType.getType(request.getSourceType())).getModuleTable());
// 构建模块树层级数量为通用逻辑
return super.buildTreeAndCountResource(relateCaseModules, true, Translator.get("api_unplanned_request"));
}
/**
* 获取关联用例模块树数量
* @param request 请求参数
* @return 模块树集合
*/
public Map<String, Long> countTree(TestCasePageProviderRequest request) {
// 统计模块数量不用传模块ID
request.setModuleIds(null);
// 目前只保留功能用例的左侧模块树方法调用, 后续其他用例根据RelateCaseType扩展
List<ModuleCountDTO> moduleCounts = extBugRelateCaseMapper.countRelateCaseModuleTree(request, false);
AssociateCaseModuleRequest moduleRequest = new AssociateCaseModuleRequest();
BeanUtils.copyBean(moduleRequest, request);
List<BaseTreeNode> relateCaseModules = extBugRelateCaseMapper.getRelateCaseModule(moduleRequest);
List<BaseTreeNode> relateCaseModuleWithCount = buildTreeAndCountResource(relateCaseModules, moduleCounts, true, Translator.get("api_unplanned_request"));
Map<String, Long> moduleCountMap = getIdCountMapByBreadth(relateCaseModuleWithCount);
long total = getAllCount(moduleCounts);
moduleCountMap.put("total", total);
return moduleCountMap;
return super.buildTreeAndCountResource(relateCaseModules, true, Translator.get(Objects.requireNonNull(CaseType.getType(request.getSourceType())).getUnPlanName()));
}
/**
@ -100,8 +100,11 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
* @param currentUser 当前用户
*/
public void relateCase(AssociateOtherCaseRequest request, boolean deleted, String currentUser) {
// 用例类型参数非法校验
this.checkCaseTypeParamIllegal(request.getSourceType());
// 目前只需根据关联条件获取功能用例ID, 后续扩展
List<String> relatedIds = functionalCaseProvider.getRelatedIdsByParam(request, deleted);
BaseAssociateCaseProvider caseProvider = AssociateCaseFactory.getInstance(request.getSourceType());
List<String> relatedIds = caseProvider.getRelatedIdsByParam(request, deleted);
// 缺陷关联用例通用逻辑
if (CollectionUtils.isEmpty(relatedIds)) {
return;
@ -114,6 +117,7 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
List<BugRelationCase> planRelatedCases = bugRelationCaseMapper.selectByExample(bugRelationCaseExample);
Map<String, String> planRelatedMap = planRelatedCases.stream().collect(Collectors.toMap(BugRelationCase::getTestPlanCaseId, BugRelationCase::getId));
bugRelationCaseExample.clear();
// 根据用例ID筛选出已直接关联缺陷的用例(防止重复关联)
bugRelationCaseExample.createCriteria().andBugIdEqualTo(request.getSourceId()).andCaseIdIn(relatedIds);
List<BugRelationCase> bugRelationCases = bugRelationCaseMapper.selectByExample(bugRelationCaseExample);
Map<String, String> bugRelatedMap = bugRelationCases.stream().collect(Collectors.toMap(BugRelationCase::getCaseId, BugRelationCase::getId));
@ -124,7 +128,7 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
continue;
}
if (planRelatedMap.containsKey(relatedId)) {
// 计划已关联, 用例ID
// 计划已关联, 用例ID
record.setId(planRelatedMap.get(relatedId));
record.setCaseId(relatedId);
record.setUpdateTime(System.currentTimeMillis());
@ -150,7 +154,6 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
* @param request 请求参数
*/
public List<BugRelateCaseDTO> page(BugRelatedCasePageRequest request) {
// 目前只查关联的功能用例类型, 后续多个用例类型SQL扩展
List<BugRelateCaseDTO> relateCases = extBugRelateCaseMapper.list(request);
if (CollectionUtils.isEmpty(relateCases)) {
return new ArrayList<>();
@ -160,7 +163,7 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
relateCases.forEach(relateCase -> {
relateCase.setProjectName(projectMap.get(relateCase.getProjectId()));
relateCase.setVersionName(versionMap.get(relateCase.getVersionId()));
relateCase.setRelateCaseTypeName(CaseType.getValue(relateCase.getRelateCaseType()));
relateCase.setRelateCaseTypeName(Translator.get(Objects.requireNonNull(CaseType.getType(relateCase.getRelateCaseType())).getType()));
});
return relateCases;
}
@ -189,12 +192,9 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
* @param caseType 用例类型
*/
public BugCaseCheckResult checkPermission(String projectId, String currentUser, String caseType) {
// 校验关联用例的查看权限, 目前只支持功能用例的查看权限, 后续支持除功能用例外的其他类型用例
if (!CaseType.FUNCTIONAL_CASE.getKey().equals(caseType)) {
// 关联的用例类型未知
return BugCaseCheckResult.builder().pass(false).msg(Translator.get("bug_relate_case_type_unknown")).build();
}
boolean hasPermission = permissionCheckService.userHasProjectPermission(currentUser, projectId, PermissionConstants.FUNCTIONAL_CASE_READ);
// 校验用例类型是否合法
this.checkCaseTypeParamIllegal(caseType);
boolean hasPermission = permissionCheckService.userHasProjectPermission(currentUser, projectId, Objects.requireNonNull(CaseType.getType(caseType)).getUsePermission());
if (!hasPermission) {
// 没有该用例的访问权限
return BugCaseCheckResult.builder().pass(false).msg(Translator.get("bug_relate_case_permission_error")).build();
@ -248,4 +248,14 @@ public class BugRelateCaseCommonService extends ModuleTreeService {
public void refreshPos(String parentId) {
}
/**
* 校验用例类型字段是否合法
* @param caseType 用例类型
*/
public void checkCaseTypeParamIllegal(String caseType) {
if (CaseType.getType(caseType) == null) {
throw new MSException(Translator.get("unknown_case_type_of_relate_case"));
}
}
}

View File

@ -4,6 +4,7 @@ import io.metersphere.bug.dto.BugCaseCheckResult;
import io.metersphere.bug.dto.request.BugRelatedCasePageRequest;
import io.metersphere.bug.dto.response.BugRelateCaseDTO;
import io.metersphere.bug.service.BugRelateCaseCommonService;
import io.metersphere.context.AssociateCaseFactory;
import io.metersphere.provider.BaseAssociateCaseProvider;
import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest;
@ -36,6 +37,7 @@ public class BugRelateCaseControllerTests extends BaseTest {
@Resource
BaseAssociateCaseProvider functionalCaseProvider;
@Resource
BugRelateCaseCommonService bugRelateCaseCommonService;
@ -85,6 +87,7 @@ public class BugRelateCaseControllerTests extends BaseTest {
request.setVersionId("default_bug_version");
request.setSourceId("default-relate-bug-id");
request.setSourceType("FUNCTIONAL");
AssociateCaseFactory.PROVIDER_MAP.put("FUNCTIONAL", functionalCaseProvider);
Mockito.when(functionalCaseProvider.getRelatedIdsByParam(request, false)).thenReturn(Collections.emptyList());
this.requestPostWithOk(BUG_CASE_RELATE, request);
request.setExcludeIds(null);
@ -173,10 +176,7 @@ public class BugRelateCaseControllerTests extends BaseTest {
@Order(8)
void testBugRelateCheckPermissionError() throws Exception {
// 非功能用例类型参数
MvcResult mvcResult = this.requestGetAndReturn(BUG_CASE_CHECK + "/100001100001/API");
BugCaseCheckResult resultData = getResultData(mvcResult, BugCaseCheckResult.class);
Assertions.assertFalse(resultData.getPass());
Assertions.assertEquals(resultData.getMsg(), Translator.get("bug_relate_case_type_unknown"));
this.requestGet(BUG_CASE_CHECK + "/100001100001/UI", status().is5xxServerError());
}
@Test

View File

@ -17,8 +17,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public class AssociateCaseProvider implements BaseAssociateCaseProvider {
@Service("FUNCTIONAL")
public class AssociateFunctionalProvider implements BaseAssociateCaseProvider {
@Resource
private FunctionalCaseService functionalCaseService;
@ -35,13 +35,13 @@ public class AssociateCaseProvider implements BaseAssociateCaseProvider {
Map<String, List<FunctionalCaseCustomFieldDTO>> caseCustomFiledMap = functionalCaseService.getCaseCustomFiledMap(ids, testCasePageProviderRequest.getProjectId());
functionalCases.forEach(functionalCase -> {
List<FunctionalCaseCustomFieldDTO> customFields = caseCustomFiledMap.get(functionalCase.getId());
List<BaseCaseCustomFieldDTO> baseCaseCustomFieldDTOS = new ArrayList<>();
List<BaseCaseCustomFieldDTO> customs = new ArrayList<>();
for (FunctionalCaseCustomFieldDTO customField : customFields) {
BaseCaseCustomFieldDTO baseCaseCustomFieldDTO = new BaseCaseCustomFieldDTO();
BeanUtils.copyBean(baseCaseCustomFieldDTO, customField);
baseCaseCustomFieldDTOS.add(baseCaseCustomFieldDTO);
customs.add(baseCaseCustomFieldDTO);
}
functionalCase.setCustomFields(baseCaseCustomFieldDTOS);
functionalCase.setCustomFields(customs);
});
return functionalCases;
}

View File

@ -1,6 +1,6 @@
package io.metersphere.functional.controller;
import io.metersphere.functional.provider.AssociateCaseProvider;
import io.metersphere.functional.provider.AssociateFunctionalProvider;
import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest;
import io.metersphere.system.base.BaseTest;
@ -22,7 +22,7 @@ import java.util.List;
public class AssociateCaseProviderTests extends BaseTest {
@Resource
AssociateCaseProvider associateCaseProvider;
AssociateFunctionalProvider functionalProvider;
@Test
@Order(1)
@ -33,17 +33,18 @@ public class AssociateCaseProviderTests extends BaseTest {
request.setVersionId("test-ver");
request.setSourceId("test-source-id");
request.setSourceType("test-source-type");
associateCaseProvider.listUnRelatedTestCaseList(request);
functionalProvider.listUnRelatedTestCaseList(request);
AssociateOtherCaseRequest associateRequest = new AssociateOtherCaseRequest();
associateRequest.setSelectAll(true);
associateRequest.setProjectId("select-case-pro");
associateRequest.setVersionId("v1.0.0");
associateRequest.setSourceId("test-source-id");
associateRequest.setSourceType("test-source-type");
associateCaseProvider.getRelatedIdsByParam(associateRequest, false);
functionalProvider.getRelatedIdsByParam(associateRequest, false);
associateRequest.setExcludeIds(List.of("select-case"));
associateCaseProvider.getRelatedIdsByParam(associateRequest, false);
functionalProvider.getRelatedIdsByParam(associateRequest, false);
associateRequest.setSelectAll(false);
associateRequest.setSelectIds(List.of("select-case"));
functionalProvider.getRelatedIdsByParam(associateRequest, false);
}
}

View File

@ -147,7 +147,7 @@
<if test="request.keyword != null">
and (
o.name like concat('%', #{request.keyword},'%')
or o.id like concat('%', #{request.keyword},'%')
or o.num like concat('%', #{request.keyword},'%')
)
</if>
<include refid="filter"/>