Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2022-present CodeStyle Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package top.codestyle.admin.common.context;

/**
* Open API 租户上下文
*
* @author CodeStyle Team
* @since 1.0.0
*/
public class ApiTenantContextHolder {

private static final ThreadLocal<Long> TENANT_ID_HOLDER = new ThreadLocal<>();

private ApiTenantContextHolder() {
}

public static void setTenantId(Long tenantId) {
TENANT_ID_HOLDER.set(tenantId);
}

public static Long getTenantId() {
return TENANT_ID_HOLDER.get();
}

public static void clear() {
TENANT_ID_HOLDER.remove();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import top.codestyle.admin.common.base.model.entity.BaseDO;
import top.codestyle.admin.common.base.model.entity.TenantBaseDO;
import top.codestyle.admin.common.enums.DisEnableStatusEnum;
import top.continew.starter.encrypt.field.annotation.FieldEncrypt;

Expand All @@ -34,7 +34,7 @@
*/
@Data
@TableName("sys_app")
public class AppDO extends BaseDO {
public class AppDO extends TenantBaseDO {

@Serial
private static final long serialVersionUID = 1L;
Expand Down Expand Up @@ -82,4 +82,4 @@ public boolean isExpired() {
}
return LocalDateTime.now().isAfter(expireTime);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,10 @@ public class AppReq implements Serializable {
*/
@Schema(hidden = true)
private String secretKey;
}

/**
* 租户 ID
*/
@Schema(hidden = true)
private Long tenantId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import top.codestyle.admin.common.base.service.BaseServiceImpl;
import top.codestyle.admin.common.context.UserContextHolder;
import top.codestyle.admin.open.mapper.AppMapper;
import top.codestyle.admin.open.model.entity.AppDO;
import top.codestyle.admin.open.model.query.AppQuery;
Expand All @@ -39,6 +41,7 @@
* @since 2024/10/17 16:03
*/
@Service
@Slf4j
public class AppServiceImpl extends BaseServiceImpl<AppMapper, AppDO, AppResp, AppDetailResp, AppQuery, AppReq> implements AppService {

@Override
Expand All @@ -48,6 +51,13 @@ public void beforeCreate(AppReq req) {
.replace(StringConstants.PLUS, StringConstants.EMPTY)
.substring(0, 30));
req.setSecretKey(this.generateSecret());
Long tenantId = UserContextHolder.getContext().getTenantId();
log.info("创建 App 前获取当前用户租户: tenantId={}", tenantId);
if (tenantId == null) {
tenantId = 0L;
}
req.setTenantId(tenantId);
log.info("创建 App 写入 tenantId: tenantId={}, accessKey={}", tenantId, req.getAccessKey());
}

@Override
Expand Down Expand Up @@ -82,4 +92,4 @@ private String generateSecret() {
.replace(StringConstants.SLASH, StringConstants.EMPTY)
.replace(StringConstants.PLUS, StringConstants.EMPTY);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.sign.template.SaSignTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import top.codestyle.admin.common.context.ApiTenantContextHolder;
import top.codestyle.admin.common.enums.DisEnableStatusEnum;
import top.codestyle.admin.open.model.entity.AppDO;
import top.codestyle.admin.open.service.AppService;
Expand All @@ -35,6 +37,7 @@
* @since 2024/10/17 16:03
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class OpenApiSignTemplate extends SaSignTemplate {

Expand All @@ -58,12 +61,22 @@ public void checkParamMap(Map<String, String> paramMap) {
ValidationUtils.throwIfNull(app, "accessKey无效");
ValidationUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, app.getStatus(), "应用已被禁用, 请联系管理员");
ValidationUtils.throwIf(app.isExpired(), "应用已过期, 请联系管理员");
log.info("OpenAPI 验签开始: accessKey={}, dbTenantId={}", accessKeyValue, app.getTenantId());
if (app.getTenantId() == null) {
app.setTenantId(0L);
log.info("OpenAPI 验签租户兜底后: accessKey={}, effectiveTenantId={}", accessKeyValue, app.getTenantId());
}

// 依次校验三个参数
super.checkTimestamp(Long.parseLong(timestampValue));
super.checkNonce(nonceValue);
paramMap.put(key, app.getSecretKey());
super.checkSign(paramMap, signValue);

// 记录 API 请求租户上下文(基于 AK/SK 绑定的应用)
ApiTenantContextHolder.setTenantId(app.getTenantId());
log.info("OpenAPI 验签完成并写入 ApiTenantContextHolder: accessKey={}, tenantId={}", accessKeyValue, app
.getTenantId());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import top.codestyle.admin.search.model.SearchRequest;
import top.codestyle.admin.search.model.SearchResult;
import top.codestyle.admin.search.service.SearchService;
import top.codestyle.admin.search.util.SearchTenantUtils;

import java.util.List;

Expand All @@ -33,6 +35,7 @@
* @author CodeStyle Team
* @since 1.0.0
*/
@Slf4j
@Tag(name = "检索 API")
@RestController
@RequiredArgsConstructor
Expand Down Expand Up @@ -80,6 +83,9 @@ public List<SearchResult> openApiSearch(@RequestParam String query,
SearchRequest request = new SearchRequest();
request.setQuery(query);
request.setTopK(topK);
Long tenantId = SearchTenantUtils.resolveCurrentTenantId();
request.setTenantId(tenantId);
log.info("OpenAPI 搜索请求写入 tenantId: query={}, topK={}, tenantId={}", query, topK, tenantId);
return searchService.search(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ public class SearchRequest {

@Schema(description = "关键词检索权重(混合检索时,0-1之间)", example = "0.4")
private Double keywordWeight = 0.4;

@Schema(hidden = true)
private Long tenantId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package top.codestyle.admin.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
Expand All @@ -29,8 +30,8 @@
import top.codestyle.admin.search.model.SearchResult;
import top.codestyle.admin.search.model.SearchSourceType;
import top.codestyle.admin.search.service.ElasticsearchSearchService;
import top.codestyle.admin.search.util.SearchTenantUtils;
import top.continew.starter.core.exception.BusinessException;

import java.io.IOException;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -60,18 +61,21 @@ public class ElasticsearchSearchServiceImpl implements ElasticsearchSearchServic
@Override
public List<SearchResult> search(SearchRequest request) {
try {
log.debug("开始 ES 检索,查询: {}, topK: {}", request.getQuery(), request.getTopK());
log.info("开始 ES 检索,查询: {}, topK: {}, requestTenantId={}", request.getQuery(), request.getTopK(), request
.getTenantId());

// 1. 构建查询
co.elastic.clients.elasticsearch.core.SearchRequest esRequest = buildSearchRequest(request);
log.info("ES 检索请求已构建: index={}, tenantFilter={}, query={}", properties.getElasticsearch()
.getIndex(), request.getTenantId() != null ? request.getTenantId() : 0L, request.getQuery());

// 2. 执行查询
SearchResponse<Map> response = esClient.search(esRequest, Map.class);

// 3. 转换结果
List<SearchResult> results = convertResults(response);

log.debug("ES 检索完成,返回 {} 条结果", results.size());
log.info("ES 检索完成,返回 {} 条结果", results.size());
return results;

} catch (IOException e) {
Expand All @@ -86,19 +90,22 @@ public List<SearchResult> search(SearchRequest request) {
/**
* 构建 ES 查询请求
* <p>
* 多字段加权检索
* - title^3: 标题权重最高
* - content^2: 内容权重次之
* - tags^1: 标签权重最低
* 模板级元数据检索
* - name/artifactId: 标题和模板标识
* - description/summary: 模板描述摘要
* - tags.text: 中文标签检索
*/
private co.elastic.clients.elasticsearch.core.SearchRequest buildSearchRequest(SearchRequest request) {
Long tenantId = SearchTenantUtils.resolveSearchTenantId(request);
return co.elastic.clients.elasticsearch.core.SearchRequest.of(s -> s.index(properties.getElasticsearch()
.getIndex())
.query(q -> q.multiMatch(m -> m.query(request.getQuery())
.fields("title^3", "content^2", "tags^1") // 字段加权
.type(TextQueryType.BestFields)))
.query(q -> q.bool(b -> b.must(m -> m.multiMatch(mm -> mm.query(request.getQuery())
.fields("name^4", "artifactId.text^3", "description^3", "summary^2", "tags.text^3")
.type(TextQueryType.BestFields)
.operator(Operator.Or))).filter(f -> f.term(t -> t.field("tenantId").value(String.valueOf(tenantId))))))
.size(request.getTopK())
.highlight(h -> h.fields("content", f -> f.numberOfFragments(1).fragmentSize(200))));
.highlight(h -> h.fields("description", f -> f.numberOfFragments(1).fragmentSize(160))
.fields("summary", f -> f.numberOfFragments(1).fragmentSize(160))));
}

/**
Expand All @@ -118,34 +125,45 @@ private SearchResult convertHit(Hit<Map> hit) {
.id(hit.id())
.sourceType(SearchSourceType.ELASTICSEARCH)
.title(getStringValue(source, "title"))
.content(getStringValue(source, "content"))
.snippet(getStringValue(source, "snippet"))
.content(getStringValue(source, "description"))
.snippet(extractSnippet(hit, source))
.score(hit.score())
.highlight(extractHighlight(hit))
.metadata(source) // 完整的 metadata,包含所有字段(包括非索引字段)
// MCP 必要字段(从 ES 索引中提取)
.metadata(source)
.groupId(getStringValue(source, "groupId"))
.artifactId(getStringValue(source, "artifactId"))
.version(getStringValue(source, "version"))
.fileType(getStringValue(source, "fileType"))
.build();
// 注意:filePath, filename, sha256 等非索引字段
// 已经包含在 metadata 中,通过 SearchResult 的 getter 方法访问
}

/**
* 提取高亮片段
*/
private String extractHighlight(Hit<Map> hit) {
if (hit.highlight() != null && hit.highlight().containsKey("content")) {
List<String> fragments = hit.highlight().get("content");
if (!fragments.isEmpty()) {
return fragments.get(0);
if (hit.highlight() != null) {
if (hit.highlight().containsKey("description") && !hit.highlight().get("description").isEmpty()) {
return hit.highlight().get("description").get(0);
}
if (hit.highlight().containsKey("summary") && !hit.highlight().get("summary").isEmpty()) {
return hit.highlight().get("summary").get(0);
}
}
return null;
}

private String extractSnippet(Hit<Map> hit, Map<String, Object> source) {
String highlight = extractHighlight(hit);
if (highlight != null) {
return highlight;
}
String summary = getStringValue(source, "summary");
if (summary != null) {
return summary;
}
return getStringValue(source, "description");
}

/**
* 安全获取字符串值
*/
Expand Down
Loading
Loading