feat(超算专区): 优化全局搜索中项目搜索逻辑

1. 项目搜索时基于Gitlink搜索功能实现
2. 支持项目描述搜索
3. 支持项目标签搜索
This commit is contained in:
欧涛 2026-04-27 14:16:01 +08:00
parent e2347e6609
commit 6830387bb8
12 changed files with 319 additions and 100 deletions

View File

@ -211,6 +211,17 @@ public class HttpAPIService {
return doRequest(httpDelete, headers);
}
/**
* 不带Header的get请求
*
* @param url
* @return
* @throws Exception
*/
public JSONObject doGet(String url) throws Exception {
return doGet(url, null);
}
/**
* 带Header的get请求
*

View File

@ -246,7 +246,7 @@ public class OpenController extends BaseController {
@GetMapping("/{zoneId}/member/homePageList")
@ApiOperation("获取首页特色专区会员列表")
public GenericsTableDataInfo<ZoneMemberDataVo> homePageMemberList(@ApiParam(name = "zoneId", value = "专区Id", required = true) @PathVariable("zoneId") Long zoneId) {
List<ZoneMemberDataVo> list = zoneMemberService.selectZoneMemberHomePageMemberList(zoneId,true);
List<ZoneMemberDataVo> list = zoneMemberService.selectZoneMemberHomePageMemberList(zoneId, true);
return getGenericsDataTable(list);
}
@ -421,38 +421,15 @@ public class OpenController extends BaseController {
private IGlobalSearchService globalSearchService;
/**
* 全局搜索接口GET方法
* 全局搜索接口POST方法
*
* @param keyword 搜索关键词
* @param types 搜索类型逗号分隔
* @param zoneId 专区ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 搜索结果
*/
@ApiOperation("全局搜索GET")
@GetMapping("/global")
public GenericsTableDataInfo<GlobalSearchResult> globalSearchGet(
@ApiParam("搜索关键词") @RequestParam String keyword,
@ApiParam("搜索类型zone_project项目, resource资源, member会员") @RequestParam(required = false) String types,
@ApiParam("专区ID") @RequestParam Long zoneId,
@ApiParam("页码") @RequestParam(defaultValue = "1") Integer pageNum,
@ApiParam("每页数量") @RequestParam(defaultValue = "20") Integer pageSize) {
GlobalSearchRequest request = new GlobalSearchRequest();
request.setKeyword(keyword);
request.setPageNum(pageNum);
request.setPageSize(pageSize);
request.setZoneId(zoneId);
if (types != null && !types.isEmpty()) {
request.setTypes(java.util.Arrays.asList(types.split(",")));
}
logger.info("全局搜索请求(GET): keyword={}, types={}", keyword, types);
GlobalSearchResponse response = globalSearchService.globalSearch(request);
return new GenericsTableDataInfo<>(response.getResults(),response.getTotal());
@PostMapping("/global")
public GenericsTableDataInfo<GlobalSearchResult> globalSearchGet(@RequestBody GlobalSearchRequest globalSearchRequest) {
logger.info("全局搜索请求(GET): keyword={}, types={}", globalSearchRequest.getKeyword(), globalSearchRequest.getTypes());
GlobalSearchResponse response = globalSearchService.globalSearch(globalSearchRequest);
return new GenericsTableDataInfo<>(response.getResults(), response.getTotal());
}
}

View File

@ -183,4 +183,6 @@ public interface ZoneMemberMapper {
@Param("zoneId") Long zoneId,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
Long globalSearchCount(String keyword, Long zoneId);
}

View File

@ -174,11 +174,9 @@ public interface ZoneProjectMapper {
*
* @param keyword 搜索关键词
* @param zoneId 专区ID可选
* @param deptId 组织ID可选
* @return 结果数量
*/
Long globalSearchCount(@Param("keyword") String keyword, @Param("zoneId") Long zoneId,
@Param("deptId") Long deptId);
Long globalSearchCount(@Param("keyword") String keyword, @Param("zoneId") Long zoneId);
/**
* 全局搜索 - 全文索引查询含评分
@ -194,4 +192,15 @@ public interface ZoneProjectMapper {
@Param("zoneId") Long zoneId,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/**
* 获取专区下项目项目gitlink id列表
*
* @param zoneId 专区id
* @param projectTypeId 专区类型id
* @return 项目gitlink id列表
*/
List<String> selectZoneProjectGitlinkIdList(@Param("zoneId") Long zoneId, @Param("projectTypeId") Long projectTypeId);
List<ZoneProject> selectZoneProjectListFromGitlinkProjectIdList(@Param("zoneId") Long zoneId, @Param("gitlinkProjectIdList") List<Long> gitlinkProjectIdList);
}

View File

@ -0,0 +1,13 @@
package com.microservices.zone.search.dto;
import lombok.Data;
import java.util.List;
@Data
public class GitlinkSearchDataVo {
private Long total=0L;
private List<GitlinkSearchVo> gitlinkSearchVoList;
}

View File

@ -0,0 +1,73 @@
package com.microservices.zone.search.dto;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.zone.project.domain.vo.ZoneProjectDataVo;
import com.microservices.zone.search.utils.SearchUtils;
import lombok.Data;
import java.util.List;
@Data
public class GitlinkSearchVo {
private String id;
private Long instanceId;
private String title;
private String content;
private String userName;
private Long type;
private Long star;
private Long fork;
private Long watch;
private String createTime;
private String updateTime;
private List<String> topics;
private Long praisesCount;
private Long forkedCount;
private Long watchersCount;
private String url;
private Double score;
public GlobalSearchResult toGlobalSearchResult(ZoneProjectDataVo zoneProjectDataVo, String topic) {
GlobalSearchResult globalSearchResult = new GlobalSearchResult();
globalSearchResult.setTitle(replaceHighlightGitlinkSearch(this.title));
globalSearchResult.setContent(replaceHighlightGitlinkSearch(this.content));
globalSearchResult.setScore(this.score);
globalSearchResult.setId(zoneProjectDataVo.getId());
globalSearchResult.setType("zone_project");
globalSearchResult.setDeptId(zoneProjectDataVo.getZoneId());
globalSearchResult.setCreateTime(globalSearchResult.getCreateTime());
globalSearchResult.setZoneId(zoneProjectDataVo.getZoneId());
JSONObject extendData = zoneProjectDataVo.getProjectProperties();
String topics = extendData.getString("topics");
if (StringUtils.isNotEmpty(topics)) {
extendData.put("topics", SearchUtils.highlightKeyword(topics, topic));
}
globalSearchResult.setExtendData(extendData);
return globalSearchResult;
}
private String replaceHighlightGitlinkSearch(String text) {
if (StringUtils.isNotEmpty(text)) {
text = text.replace("<span class=\"highlightByGitlinkSearch\">", "<em>").replace("</span>", "</em>");
}
return text;
}
}

View File

@ -1,5 +1,7 @@
package com.microservices.zone.search.dto;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.StringUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -48,4 +50,23 @@ public class GlobalSearchRequest implements Serializable {
*/
@ApiModelProperty(value = "每页数量")
private Integer pageSize = 20;
@ApiModelProperty(value = "额外搜索参数项目类型projectTypeId,topic")
private JSONObject extraSearchParam;
public String toProjectEsSearchString(String gitlinkSearchUrl, String gitlinkProjectIds, String topic) {
String esSearchString= gitlinkSearchUrl +
"/search" +
"?" +
"page=" + this.pageNum +
"&size=" + this.pageSize +
"&term=" + this.keyword +
"&type=" + 1 +
"&instanceIds=" + gitlinkProjectIds;
if(StringUtils.isNotEmpty(topic)){
esSearchString+="&topic=" + topic;
}
return esSearchString;
}
}

View File

@ -0,0 +1,29 @@
package com.microservices.zone.search.dto;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 全局搜索结果对象
*
* @author microservices
*/
@Data
@ApiModel("全局搜索结果对象")
public class GlobalSearchResultDataVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "总数")
private Long total;
@ApiModelProperty(value = "搜索结果")
private List<GlobalSearchResult> globalSearchResultList;
}

View File

@ -1,9 +1,11 @@
package com.microservices.zone.search.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservices.common.core.domain.R;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.httpClient.service.HttpAPIService;
import com.microservices.common.redis.service.RedisService;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.RemoteFileService;
@ -11,28 +13,27 @@ import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.model.LoginUser;
import com.microservices.zone.project.domain.ZoneProject;
import com.microservices.zone.project.domain.ZoneProjectWithScore;
import com.microservices.zone.project.domain.vo.GitLinkProjectSearchVo;
import com.microservices.zone.project.domain.vo.ZoneProjectDataVo;
import com.microservices.zone.project.mapper.ZoneProjectMapper;
import com.microservices.zone.project.service.IZoneProjectService;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.domain.ZoneResourceWithScore;
import com.microservices.zone.resource.mapper.ZoneResourceMapper;
import com.microservices.zone.member.domain.ZoneMember;
import com.microservices.zone.member.domain.ZoneMemberWithScore;
import com.microservices.zone.member.mapper.ZoneMemberMapper;
import com.microservices.zone.search.dto.GlobalSearchRequest;
import com.microservices.zone.search.dto.GlobalSearchResponse;
import com.microservices.zone.search.dto.GlobalSearchResult;
import com.microservices.zone.search.dto.*;
import com.microservices.zone.search.service.IGlobalSearchService;
import com.microservices.zone.search.utils.SearchUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.commons.config.DefaultsBindHandlerAdvisor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
@ -62,27 +63,23 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
@Autowired
private IZoneProjectService projectService;
@Autowired
private HttpAPIService httpAPIService;
@Value("${http.gitLinkSearchUrl:}")
public String gitLinkSearchUrl;
private static final String SEARCH_CACHE_PREFIX = "search:cache:";
private static final long CACHE_EXPIRE_SECONDS = 300; // 5分钟
@Autowired
private DefaultsBindHandlerAdvisor.MappingsProvider mappingsProvider;
@Override
public GlobalSearchResponse globalSearch(GlobalSearchRequest request) {
long startTime = System.currentTimeMillis();
// 参数校验
if (StringUtils.isEmpty(request.getKeyword())) {
return buildEmptyResponse(request);
}
// 1. 尝试从缓存获取仅第一页
if (request.getPageNum() == 1) {
String cacheKey = buildCacheKey(request);
GlobalSearchResponse cached = redisService.getCacheObject(SEARCH_CACHE_PREFIX + cacheKey);
if (cached != null) {
cached.setSearchTime(System.currentTimeMillis() - startTime);
return cached;
}
if (request.getZoneId() == null) {
throw new ServiceException("专区id不能为空");
}
// 2. 并行搜索各类型
@ -97,26 +94,32 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
if (searchTypes == null || searchTypes.isEmpty()) {
searchTypes = Arrays.asList("zone_project", "resource", "member");
}
Long searchTotal = 0L;
// 搜索Zone项目
if (searchTypes.contains("zone_project")) {
List<GlobalSearchResult> zoneProjectResults = searchZoneProjects(request);
allResults.addAll(zoneProjectResults);
typeCounts.put("zone_project", (long) zoneProjectResults.size());
GlobalSearchResultDataVo resultDataVo = searchZoneProjects(request);
allResults.addAll(resultDataVo.getGlobalSearchResultList());
typeCounts.put("zone_project", resultDataVo.getTotal());
searchTotal += resultDataVo.getTotal();
}
// 搜索Zone资源
if (searchTypes.contains("resource")) {
List<GlobalSearchResult> resourceResults = searchResources(request);
allResults.addAll(resourceResults);
typeCounts.put("resource", (long) resourceResults.size());
Long searchResourceCount = zoneResourceMapper.globalSearchCount(request.getKeyword(), request.getZoneId());
typeCounts.put("resource", searchResourceCount);
searchTotal += searchResourceCount;
}
// 搜索Zone会员
if (searchTypes.contains("member")) {
List<GlobalSearchResult> memberResults = searchZoneMembers(request);
allResults.addAll(memberResults);
typeCounts.put("member", (long) memberResults.size());
Long searchMemberCount = zoneMemberMapper.globalSearchCount(request.getKeyword(), request.getZoneId());
typeCounts.put("member", searchMemberCount);
searchTotal += searchMemberCount;
}
// 3. 按相关度评分排序
@ -136,7 +139,7 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
// 5. 构建响应
GlobalSearchResponse response = new GlobalSearchResponse();
response.setTotal((long) total);
response.setTotal(searchTotal);
response.setResults(pagedResults);
response.setTypeCounts(typeCounts);
response.setKeyword(request.getKeyword());
@ -144,16 +147,6 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
response.setPageSize(request.getPageSize());
response.setSearchTime(System.currentTimeMillis() - startTime);
// 6. 缓存结果仅第一页
if (request.getPageNum() == 1) {
String cacheKey = buildCacheKey(request);
redisService.setCacheObject(
SEARCH_CACHE_PREFIX + cacheKey,
response,
CACHE_EXPIRE_SECONDS,
TimeUnit.SECONDS);
}
// 7. 异步记录搜索日志
recordSearchLog(request, response, loginUser);
@ -182,29 +175,83 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
/**
* 搜索Zone项目
*/
private List<GlobalSearchResult> searchZoneProjects(GlobalSearchRequest request) {
private GlobalSearchResultDataVo searchZoneProjects(GlobalSearchRequest request) {
GlobalSearchResultDataVo globalSearchResultDataVo = new GlobalSearchResultDataVo();
List<GlobalSearchResult> results = new ArrayList<>();
Long total = 0L;
try {
List<ZoneProjectWithScore> projects = zoneProjectMapper.globalSearchWithScore(
request.getKeyword(),
request.getZoneId(),
(request.getPageNum() - 1) * request.getPageSize(),
request.getPageSize()
);
List<ZoneProject> zoneProjectList = projects.stream().map(x -> (ZoneProject) x).collect(Collectors.toList());
List<ZoneProjectDataVo> projectDataVoList = projectService.toZoneProjectDataVoList(zoneProjectList);
if (StringUtils.isNotEmpty(gitLinkSearchUrl)) {
Long projectTypeId = null;
String topic = null;
if (request.getExtraSearchParam() != null) {
projectTypeId = request.getExtraSearchParam().getLong("projectTypeId");
topic = request.getExtraSearchParam().getString("topic");
}
List<String> gitlinkProjectIds = zoneProjectMapper.selectZoneProjectGitlinkIdList(request.getZoneId(), projectTypeId);
for (ZoneProjectWithScore project : projects) {
ZoneProjectDataVo zoneProjectDataVo = projectDataVoList.stream().filter(x -> x.getId().equals(project.getId())).findFirst().orElse(null);
GlobalSearchResult result = project.toGlobalSearchResult(zoneProjectDataVo);
result.setTitle(highlightKeyword(result.getTitle(), request.getKeyword()));
result.setContent(highlightKeyword(result.getContent(), request.getKeyword()));
results.add(result);
JSONObject searchRes = httpAPIService.doGet(request.toProjectEsSearchString(gitLinkSearchUrl, String.join(",", gitlinkProjectIds), topic));
GitlinkSearchDataVo gitlinkSearchDataVos = gitlinkSearchResToGitlinkSearchDataVo(searchRes);
total = gitlinkSearchDataVos.getTotal();
if (gitlinkSearchDataVos.getTotal() > 0) {
List<GitlinkSearchVo> gitlinkSearchVoList = gitlinkSearchDataVos.getGitlinkSearchVoList();
List<Long> gitlinkProjectIdList = gitlinkSearchVoList.stream().map(GitlinkSearchVo::getInstanceId).collect(Collectors.toList());
List<ZoneProject> zoneProjectList = zoneProjectMapper.selectZoneProjectListFromGitlinkProjectIdList(request.getZoneId(), gitlinkProjectIdList);
List<ZoneProjectDataVo> projectDataVoList = projectService.toZoneProjectDataVoList(zoneProjectList);
for (GitlinkSearchVo gitlinkSearchVo : gitlinkSearchVoList) {
ZoneProjectDataVo zoneProjectDataVo = projectDataVoList.stream().filter(x -> x.getGitlinkProjectId().equals(gitlinkSearchVo.getInstanceId())).findFirst().orElse(null);
if (zoneProjectDataVo != null) {
GlobalSearchResult result = gitlinkSearchVo.toGlobalSearchResult(zoneProjectDataVo,topic);
results.add(result);
}
}
}
} else {
List<ZoneProjectWithScore> projects = zoneProjectMapper.globalSearchWithScore(
request.getKeyword(),
request.getZoneId(),
(request.getPageNum() - 1) * request.getPageSize(),
request.getPageSize()
);
total = zoneProjectMapper.globalSearchCount(request.getKeyword(), request.getZoneId());
List<ZoneProject> zoneProjectList = projects.stream().map(x -> (ZoneProject) x).collect(Collectors.toList());
List<ZoneProjectDataVo> projectDataVoList = projectService.toZoneProjectDataVoList(zoneProjectList);
for (ZoneProjectWithScore project : projects) {
ZoneProjectDataVo zoneProjectDataVo = projectDataVoList.stream().filter(x -> x.getId().equals(project.getId())).findFirst().orElse(null);
GlobalSearchResult result = project.toGlobalSearchResult(zoneProjectDataVo);
result.setTitle(SearchUtils.highlightKeyword(result.getTitle(), request.getKeyword()));
result.setContent(SearchUtils.highlightKeyword(result.getContent(), request.getKeyword()));
results.add(result);
}
}
} catch (Exception e) {
System.err.println("搜索Zone项目失败: " + e.getMessage());
}
return results;
globalSearchResultDataVo.setTotal(total);
globalSearchResultDataVo.setGlobalSearchResultList(results);
return globalSearchResultDataVo;
}
private GitlinkSearchDataVo gitlinkSearchResToGitlinkSearchDataVo(JSONObject gitlinkSearchRes) {
GitlinkSearchDataVo gitlinkSearchDataVo = new GitlinkSearchDataVo();
List<GitlinkSearchVo> results = new ArrayList<>();
if (gitlinkSearchRes != null && gitlinkSearchRes.getJSONObject("data") != null) {
JSONObject data = gitlinkSearchRes.getJSONObject("data");
if (data.getLong("total") == null || data.getLong("total") == 0) {
gitlinkSearchDataVo.setGitlinkSearchVoList(results);
return gitlinkSearchDataVo;
}
gitlinkSearchDataVo.setTotal(data.getLong("total"));
JSONArray rows = data.getJSONArray("rows");
for (int i = 0; i < rows.size(); i++) {
JSONObject row = rows.getJSONObject(i);
results.add(row.toJavaObject(GitlinkSearchVo.class));
}
gitlinkSearchDataVo.setGitlinkSearchVoList(results);
}
return gitlinkSearchDataVo;
}
/**
@ -224,8 +271,8 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
GlobalSearchResult result = new GlobalSearchResult();
result.setType("resource");
result.setId(resource.getId());
result.setTitle(highlightKeyword(resource.getName(), request.getKeyword()));
result.setContent(highlightKeyword(resource.getSummary(), request.getKeyword()));
result.setTitle(SearchUtils.highlightKeyword(resource.getName(), request.getKeyword()));
result.setContent(SearchUtils.highlightKeyword(resource.getSummary(), request.getKeyword()));
result.setDeptId(resource.getDeptId());
result.setZoneId(resource.getZoneId());
result.setCreateTime(resource.getCreateTime());
@ -256,7 +303,7 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
.map(file -> {
JSONObject fileInfo = new JSONObject();
fileInfo.put("fileId", file.getFileId());
fileInfo.put("fileName", highlightKeyword(file.getFileOriginName(), request.getKeyword()));
fileInfo.put("fileName", SearchUtils.highlightKeyword(file.getFileOriginName(), request.getKeyword()));
fileInfo.put("fileSize", file.getFileSizeInfo());
return fileInfo;
})
@ -299,7 +346,7 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
GlobalSearchResult result = new GlobalSearchResult();
result.setType("member");
result.setId(member.getId());
result.setTitle(highlightKeyword(member.getName(), request.getKeyword()));
result.setTitle(SearchUtils.highlightKeyword(member.getName(), request.getKeyword()));
// 构建内容用户名 + 会员介绍
StringBuilder content = new StringBuilder();
@ -312,7 +359,7 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
}
content.append(member.getIntroduction());
}
result.setContent(highlightKeyword(content.toString(), request.getKeyword()));
result.setContent(SearchUtils.highlightKeyword(content.toString(), request.getKeyword()));
result.setDeptId(member.getDeptId());
result.setZoneId(member.getZoneId());
@ -378,14 +425,4 @@ public class GlobalSearchServiceImpl implements IGlobalSearchService {
response.setSearchTime(0L);
return response;
}
/**
* 将文本中的关键词用 <em> 标签包裹不区分大小写
*/
private String highlightKeyword(String text, String keyword) {
if (StringUtils.isEmpty(text) || StringUtils.isEmpty(keyword)) {
return text;
}
return text.replaceAll("(?i)(" + Pattern.quote(keyword) + ")", "<em>$1</em>");
}
}

View File

@ -0,0 +1,19 @@
package com.microservices.zone.search.utils;
import com.microservices.common.core.utils.StringUtils;
import java.util.regex.Pattern;
public class SearchUtils {
/**
* 将文本中的关键词用 <em> 标签包裹不区分大小写
*/
public static String highlightKeyword(String text, String keyword) {
if (StringUtils.isEmpty(text) || StringUtils.isEmpty(keyword)) {
return text;
}
return text.replaceAll("(?i)(" + Pattern.quote(keyword) + ")", "<em>$1</em>");
}
}

View File

@ -454,4 +454,17 @@
ORDER BY search_score DESC, zm.create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
<select id="globalSearchCount" resultType="java.lang.Long">
SELECT COUNT(*)
FROM zone_member zm
left join sys_user u on u.user_id = zm.user_id
WHERE zm.zone_id = #{zoneId}
AND zm.is_audit = '1'
AND zm.del_flag = '0'
AND (
MATCH(zm.name) AGAINST(#{keyword} IN NATURAL LANGUAGE MODE)
OR zm.name LIKE CONCAT('%', #{keyword}, '%')
OR u.user_name LIKE CONCAT('%', #{keyword}, '%')
)
</select>
</mapper>

View File

@ -354,4 +354,19 @@
AND zp.dept_id = #{deptId}
</if>
</select>
<select id="selectZoneProjectGitlinkIdList" resultType="java.lang.String">
select gitlink_project_id from zone_project
where zone_id=#{zoneId}
<if test="projectTypeId != null">
AND project_type_id = #{projectTypeId}
</if>
</select>
<select id="selectZoneProjectListFromGitlinkProjectIdList" resultMap="ZoneProjectWithScoreResult">
<include refid="selectZoneProjectVo"/>
where zone_id=#{zoneId}
and gitlink_project_id in
<foreach item="gitlinkProjectId" collection="gitlinkProjectIdList" open="(" separator="," close=")">
#{gitlinkProjectId}
</foreach>
</select>
</mapper>