feat: 收口知识库分享链路
- 新增 shareKey 单参数 URL 分享页与失效页 - 新增知识库分享后端鉴权、审计与迁移脚本 - 在访问令牌中增加知识库分享授权入口
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package tech.easyflow.ai.constants;
|
||||
|
||||
/**
|
||||
* 知识库分享错误码定义。
|
||||
*/
|
||||
public final class KnowledgeShareErrorCode {
|
||||
|
||||
public static final int KNOWLEDGE_SHARE_EXPIRED = 4601;
|
||||
public static final int KNOWLEDGE_SHARE_INVALID = 4602;
|
||||
public static final int KNOWLEDGE_SHARE_FORBIDDEN = 4603;
|
||||
|
||||
private KnowledgeShareErrorCode() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* API 分享授权请求。
|
||||
*/
|
||||
public class KnowledgeShareApiGrantRequest implements Serializable {
|
||||
|
||||
private BigInteger apiKeyId;
|
||||
private BigInteger knowledgeId;
|
||||
private Set<String> actionScopes;
|
||||
|
||||
public BigInteger getApiKeyId() {
|
||||
return apiKeyId;
|
||||
}
|
||||
|
||||
public void setApiKeyId(BigInteger apiKeyId) {
|
||||
this.apiKeyId = apiKeyId;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public Set<String> getActionScopes() {
|
||||
return actionScopes;
|
||||
}
|
||||
|
||||
public void setActionScopes(Set<String> actionScopes) {
|
||||
this.actionScopes = actionScopes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 分享页受限配置更新请求。
|
||||
*/
|
||||
public class KnowledgeShareLimitedConfigRequest implements Serializable {
|
||||
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger vectorEmbedModelId;
|
||||
private BigInteger rerankModelId;
|
||||
private Boolean rerankEnable;
|
||||
private Integer docRecallMaxNum;
|
||||
private Double simThreshold;
|
||||
private Boolean rebuildVectors;
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getVectorEmbedModelId() {
|
||||
return vectorEmbedModelId;
|
||||
}
|
||||
|
||||
public void setVectorEmbedModelId(BigInteger vectorEmbedModelId) {
|
||||
this.vectorEmbedModelId = vectorEmbedModelId;
|
||||
}
|
||||
|
||||
public BigInteger getRerankModelId() {
|
||||
return rerankModelId;
|
||||
}
|
||||
|
||||
public void setRerankModelId(BigInteger rerankModelId) {
|
||||
this.rerankModelId = rerankModelId;
|
||||
}
|
||||
|
||||
public Boolean getRerankEnable() {
|
||||
return rerankEnable;
|
||||
}
|
||||
|
||||
public void setRerankEnable(Boolean rerankEnable) {
|
||||
this.rerankEnable = rerankEnable;
|
||||
}
|
||||
|
||||
public Integer getDocRecallMaxNum() {
|
||||
return docRecallMaxNum;
|
||||
}
|
||||
|
||||
public void setDocRecallMaxNum(Integer docRecallMaxNum) {
|
||||
this.docRecallMaxNum = docRecallMaxNum;
|
||||
}
|
||||
|
||||
public Double getSimThreshold() {
|
||||
return simThreshold;
|
||||
}
|
||||
|
||||
public void setSimThreshold(Double simThreshold) {
|
||||
this.simThreshold = simThreshold;
|
||||
}
|
||||
|
||||
public Boolean getRebuildVectors() {
|
||||
return rebuildVectors;
|
||||
}
|
||||
|
||||
public void setRebuildVectors(Boolean rebuildVectors) {
|
||||
this.rebuildVectors = rebuildVectors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.entity.base.KnowledgeShareBase;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享记录。
|
||||
*/
|
||||
@Table("tb_knowledge_share")
|
||||
public class KnowledgeShare extends KnowledgeShareBase {
|
||||
|
||||
@Column(ignore = true)
|
||||
private String shareUrl;
|
||||
|
||||
/**
|
||||
* 解析授权范围。
|
||||
*
|
||||
* @return 授权范围集合
|
||||
*/
|
||||
public Set<String> getPermissionScopeSet() {
|
||||
if (getPermissionSet() == null || getPermissionSet().isBlank()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> scopes = new LinkedHashSet<>();
|
||||
String[] segments = getPermissionSet().split(",");
|
||||
for (String segment : segments) {
|
||||
if (segment == null || segment.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
scopes.add(segment.trim().toUpperCase());
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入授权范围。
|
||||
*
|
||||
* @param scopes 授权范围集合
|
||||
*/
|
||||
public void setPermissionScopes(Iterable<String> scopes) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (scopes != null) {
|
||||
for (String scope : scopes) {
|
||||
if (scope == null || scope.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(scope.trim().toUpperCase());
|
||||
}
|
||||
}
|
||||
setPermissionSet(builder.toString());
|
||||
}
|
||||
|
||||
public String getShareUrl() {
|
||||
return shareUrl;
|
||||
}
|
||||
|
||||
public void setShareUrl(String shareUrl) {
|
||||
this.shareUrl = shareUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package tech.easyflow.ai.entity.base;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 知识库分享记录基础字段。
|
||||
*/
|
||||
public class KnowledgeShareBase implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "ID")
|
||||
private BigInteger id;
|
||||
|
||||
@Column(comment = "知识库ID")
|
||||
private BigInteger knowledgeId;
|
||||
|
||||
@Column(comment = "分享类型")
|
||||
private String shareType;
|
||||
|
||||
@Column(comment = "分享访问密钥哈希")
|
||||
private String shareKeyHash;
|
||||
|
||||
@Column(comment = "分享状态")
|
||||
private String status;
|
||||
|
||||
@Column(comment = "授权范围")
|
||||
private String permissionSet;
|
||||
|
||||
@Column(comment = "过期时间")
|
||||
private Date expiresAt;
|
||||
|
||||
@Column(tenantId = true, comment = "租户ID")
|
||||
private BigInteger tenantId;
|
||||
|
||||
@Column(comment = "部门ID")
|
||||
private BigInteger deptId;
|
||||
|
||||
@Column(comment = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Column(comment = "创建人")
|
||||
private BigInteger createdBy;
|
||||
|
||||
@Column(comment = "修改时间")
|
||||
private Date modified;
|
||||
|
||||
@Column(comment = "修改人")
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getShareType() {
|
||||
return shareType;
|
||||
}
|
||||
|
||||
public void setShareType(String shareType) {
|
||||
this.shareType = shareType;
|
||||
}
|
||||
|
||||
public String getShareKeyHash() {
|
||||
return shareKeyHash;
|
||||
}
|
||||
|
||||
public void setShareKeyHash(String shareKeyHash) {
|
||||
this.shareKeyHash = shareKeyHash;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getPermissionSet() {
|
||||
return permissionSet;
|
||||
}
|
||||
|
||||
public void setPermissionSet(String permissionSet) {
|
||||
this.permissionSet = permissionSet;
|
||||
}
|
||||
|
||||
public Date getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Date expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public BigInteger getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(BigInteger tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
public BigInteger getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
public void setDeptId(BigInteger deptId) {
|
||||
this.deptId = deptId;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(BigInteger createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public Date getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
public void setModified(Date modified) {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
public BigInteger getModifiedBy() {
|
||||
return modifiedBy;
|
||||
}
|
||||
|
||||
public void setModifiedBy(BigInteger modifiedBy) {
|
||||
this.modifiedBy = modifiedBy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享动作范围。
|
||||
*/
|
||||
public enum KnowledgeShareActionScope {
|
||||
|
||||
VIEW,
|
||||
SEARCH,
|
||||
CONTENT_CREATE,
|
||||
CONTENT_UPDATE,
|
||||
CONTENT_DELETE,
|
||||
IMPORT_EXPORT,
|
||||
CONFIG_UPDATE;
|
||||
|
||||
/**
|
||||
* 解析前端提交的动作范围集合。
|
||||
*
|
||||
* @param values 原始动作范围
|
||||
* @return 规范化后的动作范围集合
|
||||
*/
|
||||
public static Set<String> normalize(Iterable<String> values) {
|
||||
Set<String> scopes = new LinkedHashSet<>();
|
||||
if (values == null) {
|
||||
return scopes;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value == null || value.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
scopes.add(KnowledgeShareActionScope.valueOf(value.trim().toUpperCase()).name());
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认 URL 分享授权范围。
|
||||
*
|
||||
* @return 默认授权范围
|
||||
*/
|
||||
public static Set<String> defaultUrlScopes() {
|
||||
return new LinkedHashSet<>(Arrays.asList(
|
||||
VIEW.name(),
|
||||
SEARCH.name(),
|
||||
CONTENT_CREATE.name(),
|
||||
CONTENT_UPDATE.name(),
|
||||
CONTENT_DELETE.name(),
|
||||
IMPORT_EXPORT.name(),
|
||||
CONFIG_UPDATE.name()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认 API 分享授权范围。
|
||||
*
|
||||
* <p>产品上固定开放查看、检索、新增、更新、导入导出,不提供删除能力。</p>
|
||||
*
|
||||
* @return 默认 API 授权范围
|
||||
*/
|
||||
public static Set<String> defaultApiScopes() {
|
||||
return new LinkedHashSet<>(Arrays.asList(
|
||||
VIEW.name(),
|
||||
SEARCH.name(),
|
||||
CONTENT_CREATE.name(),
|
||||
CONTENT_UPDATE.name(),
|
||||
IMPORT_EXPORT.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 知识库分享状态。
|
||||
*/
|
||||
public enum KnowledgeShareStatus {
|
||||
ENABLED,
|
||||
DISABLED,
|
||||
REVOKED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 知识库分享类型。
|
||||
*/
|
||||
public enum KnowledgeShareType {
|
||||
URL
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.ai.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
|
||||
/**
|
||||
* 知识库分享记录映射层。
|
||||
*/
|
||||
public interface KnowledgeShareMapper extends BaseMapper<KnowledgeShare> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 知识库向量重建服务。
|
||||
*/
|
||||
public interface KnowledgeEmbeddingService {
|
||||
|
||||
/**
|
||||
* 按知识库重建向量数据。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
*/
|
||||
void rebuildKnowledgeVectors(BigInteger knowledgeId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库分享审计服务。
|
||||
*/
|
||||
public interface KnowledgeShareAuditService {
|
||||
|
||||
/**
|
||||
* 记录审计日志。
|
||||
*
|
||||
* @param accountId 操作人
|
||||
* @param actionName 操作名称
|
||||
* @param actionType 操作类型
|
||||
* @param actionUrl 操作地址
|
||||
* @param detail 扩展上下文
|
||||
*/
|
||||
void log(BigInteger accountId, String actionName, String actionType, String actionUrl, Map<String, Object> detail);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享权限服务。
|
||||
*/
|
||||
public interface KnowledgeSharePermissionService {
|
||||
|
||||
/**
|
||||
* 为系统访问令牌授予知识库 API 分享权限。
|
||||
*
|
||||
* @param apiKeyId 系统访问令牌 ID
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param actionScopes 动作范围
|
||||
*/
|
||||
void grantApiShare(BigInteger apiKeyId, BigInteger knowledgeId, Set<String> actionScopes);
|
||||
|
||||
/**
|
||||
* 按访问令牌维度开启或关闭知识库 API 分享授权。
|
||||
*
|
||||
* <p>产品固定为全量知识库的非删除范围,不向前端暴露动作粒度。</p>
|
||||
*
|
||||
* @param apiKeyId 系统访问令牌 ID
|
||||
* @param enabled 是否启用知识库分享授权
|
||||
*/
|
||||
void replaceApiShareEnabled(BigInteger apiKeyId, boolean enabled);
|
||||
|
||||
/**
|
||||
* 断言当前令牌具备知识库分享权限。
|
||||
*
|
||||
* @param apiKeyId 系统访问令牌 ID
|
||||
* @param requestUri 请求地址
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param actionScope 动作范围
|
||||
*/
|
||||
void assertApiShare(BigInteger apiKeyId, String requestUri, BigInteger knowledgeId, String actionScope);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareUrlCreateResult;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享服务。
|
||||
*/
|
||||
public interface KnowledgeShareService extends IService<KnowledgeShare> {
|
||||
|
||||
/**
|
||||
* 创建 URL 分享。
|
||||
*
|
||||
* @param knowledgeId 目标知识库
|
||||
* @param tenantId 租户 ID
|
||||
* @param deptId 部门 ID
|
||||
* @param operatorId 操作人
|
||||
* @param baseUrl 分享页基础 URL
|
||||
* @param permissionScopes 授权范围
|
||||
* @return 创建结果
|
||||
*/
|
||||
KnowledgeShareUrlCreateResult createUrlShare(
|
||||
BigInteger knowledgeId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl,
|
||||
Set<String> permissionScopes
|
||||
);
|
||||
|
||||
/**
|
||||
* 校验 URL 分享并返回上下文。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @return 鉴权上下文
|
||||
*/
|
||||
KnowledgeShareAuthContext validateUrlShare(String shareKey);
|
||||
|
||||
/**
|
||||
* 断言 URL 分享允许当前动作。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param actionScope 动作范围
|
||||
* @return 鉴权上下文
|
||||
*/
|
||||
KnowledgeShareAuthContext assertUrlShareAccess(
|
||||
String shareKey,
|
||||
BigInteger knowledgeId,
|
||||
String actionScope
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.ai.service.KnowledgeEmbeddingService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库向量重建服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeEmbeddingServiceImpl implements KnowledgeEmbeddingService {
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private DocumentChunkService documentChunkService;
|
||||
@Resource
|
||||
private FaqItemService faqItemService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
|
||||
@Override
|
||||
public void rebuildKnowledgeVectors(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
throw new BusinessException("知识库没有配置向量库");
|
||||
}
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
throw new BusinessException("知识库没有配置向量模型");
|
||||
}
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
StoreOptions storeOptions = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||
storeOptions.setIndexName(knowledge.getVectorStoreCollection());
|
||||
|
||||
if (knowledge.isFaqCollection()) {
|
||||
rebuildFaqVectors(knowledge, documentStore, storeOptions, embeddingModel);
|
||||
return;
|
||||
}
|
||||
rebuildDocumentVectors(knowledge, documentStore, storeOptions, embeddingModel);
|
||||
}
|
||||
|
||||
private void rebuildDocumentVectors(
|
||||
DocumentCollection knowledge,
|
||||
DocumentStore documentStore,
|
||||
StoreOptions storeOptions,
|
||||
EmbeddingModel embeddingModel
|
||||
) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(DocumentChunk::getDocumentCollectionId, knowledge.getId())
|
||||
.orderBy("sorting asc");
|
||||
List<DocumentChunk> chunks = documentChunkService.list(wrapper);
|
||||
List<BigInteger> ids = new ArrayList<>();
|
||||
List<com.easyagents.core.document.Document> documents = new ArrayList<>();
|
||||
for (DocumentChunk chunk : chunks) {
|
||||
ids.add(chunk.getId());
|
||||
com.easyagents.core.document.Document document = com.easyagents.core.document.Document.of(chunk.getContent());
|
||||
document.setId(chunk.getId());
|
||||
documents.add(document);
|
||||
}
|
||||
rewriteStore(documentStore, storeOptions, ids, documents);
|
||||
updateKnowledgeEmbeddingState(knowledge, embeddingModel);
|
||||
}
|
||||
|
||||
private void rebuildFaqVectors(
|
||||
DocumentCollection knowledge,
|
||||
DocumentStore documentStore,
|
||||
StoreOptions storeOptions,
|
||||
EmbeddingModel embeddingModel
|
||||
) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, knowledge.getId())
|
||||
.orderBy("order_no asc");
|
||||
List<FaqItem> faqItems = faqItemService.list(wrapper);
|
||||
List<BigInteger> ids = new ArrayList<>();
|
||||
List<com.easyagents.core.document.Document> documents = new ArrayList<>();
|
||||
for (FaqItem faqItem : faqItems) {
|
||||
ids.add(faqItem.getId());
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("问题:").append(faqItem.getQuestion());
|
||||
if (faqItem.getAnswerText() != null && !faqItem.getAnswerText().isBlank()) {
|
||||
content.append("\n答案:").append(faqItem.getAnswerText());
|
||||
}
|
||||
com.easyagents.core.document.Document document =
|
||||
com.easyagents.core.document.Document.of(content.toString());
|
||||
document.setId(faqItem.getId());
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("question", faqItem.getQuestion());
|
||||
metadata.put("answerText", faqItem.getAnswerText());
|
||||
metadata.put("categoryId", faqItem.getCategoryId());
|
||||
document.setMetadataMap(metadata);
|
||||
documents.add(document);
|
||||
}
|
||||
rewriteStore(documentStore, storeOptions, ids, documents);
|
||||
updateKnowledgeEmbeddingState(knowledge, embeddingModel);
|
||||
}
|
||||
|
||||
private void rewriteStore(
|
||||
DocumentStore documentStore,
|
||||
StoreOptions storeOptions,
|
||||
List<BigInteger> ids,
|
||||
List<com.easyagents.core.document.Document> documents
|
||||
) {
|
||||
if (!ids.isEmpty()) {
|
||||
documentStore.delete(ids, storeOptions);
|
||||
}
|
||||
if (documents.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StoreResult result = documentStore.store(documents, storeOptions);
|
||||
if (result == null || !result.isSuccess()) {
|
||||
throw new BusinessException("知识库向量重建失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateKnowledgeEmbeddingState(DocumentCollection knowledge, EmbeddingModel embeddingModel) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(knowledge.getId());
|
||||
Map<String, Object> options = knowledge.getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(knowledge.getOptions());
|
||||
options.put(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, false);
|
||||
update.setOptions(options);
|
||||
if (knowledge.getDimensionOfVectorModel() == null) {
|
||||
update.setDimensionOfVectorModel(Model.getEmbeddingDimension(embeddingModel));
|
||||
}
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.common.util.RequestUtil;
|
||||
import tech.easyflow.system.entity.SysLog;
|
||||
import tech.easyflow.system.service.SysLogService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库分享审计服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeShareAuditServiceImpl implements KnowledgeShareAuditService {
|
||||
|
||||
@Resource
|
||||
private SysLogService sysLogService;
|
||||
|
||||
@Override
|
||||
public void log(BigInteger accountId, String actionName, String actionType, String actionUrl, Map<String, Object> detail) {
|
||||
SysLog log = new SysLog();
|
||||
log.setAccountId(accountId);
|
||||
log.setActionName(actionName);
|
||||
log.setActionType(actionType);
|
||||
log.setActionUrl(actionUrl);
|
||||
log.setActionBody(detail == null ? null : JSON.toJSONString(detail));
|
||||
log.setCreated(new Date());
|
||||
log.setStatus(0);
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
log.setActionIp(RequestUtil.getIpAddress(request));
|
||||
log.setActionParams(request.getQueryString());
|
||||
}
|
||||
sysLogService.save(log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceMappingService;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享权限服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeSharePermissionServiceImpl implements KnowledgeSharePermissionService {
|
||||
|
||||
public static final String RESOURCE_TYPE_KNOWLEDGE = "KNOWLEDGE";
|
||||
|
||||
private static final Map<String, List<String>> URI_SCOPE_MAPPING = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.VIEW.name(), List.of(
|
||||
"/public-api/knowledge-share/detail",
|
||||
"/public-api/knowledge-share/document/page",
|
||||
"/public-api/knowledge-share/document/download",
|
||||
"/public-api/knowledge-share/documentChunk/page",
|
||||
"/public-api/knowledge-share/faq/page",
|
||||
"/public-api/knowledge-share/faq/detail"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.SEARCH.name(), List.of(
|
||||
"/public-api/knowledge-share/search"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.CONTENT_CREATE.name(), List.of(
|
||||
"/public-api/knowledge-share/document/import/analyze",
|
||||
"/public-api/knowledge-share/document/import/preview",
|
||||
"/public-api/knowledge-share/document/import/commit",
|
||||
"/public-api/knowledge-share/faq/save"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.CONTENT_UPDATE.name(), List.of(
|
||||
"/public-api/knowledge-share/documentChunk/update",
|
||||
"/public-api/knowledge-share/faq/update"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.CONTENT_DELETE.name(), List.of(
|
||||
"/public-api/knowledge-share/document/remove",
|
||||
"/public-api/knowledge-share/documentChunk/remove",
|
||||
"/public-api/knowledge-share/faq/remove"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.IMPORT_EXPORT.name(), List.of(
|
||||
"/public-api/knowledge-share/faq/importExcel",
|
||||
"/public-api/knowledge-share/faq/exportExcel",
|
||||
"/public-api/knowledge-share/faq/downloadImportTemplate"
|
||||
));
|
||||
}
|
||||
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
@Resource
|
||||
private SysApiKeyResourceService resourceService;
|
||||
@Resource
|
||||
private SysApiKeyResourceMappingService mappingService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void grantApiShare(BigInteger apiKeyId, BigInteger knowledgeId, Set<String> actionScopes) {
|
||||
if (apiKeyId == null) {
|
||||
throw new BusinessException("系统访问令牌不能为空");
|
||||
}
|
||||
if (knowledgeId == null) {
|
||||
throw new BusinessException("知识库不能为空");
|
||||
}
|
||||
SysApiKey apiKey = sysApiKeyService.getById(apiKeyId);
|
||||
if (apiKey == null) {
|
||||
throw new BusinessException("系统访问令牌不存在");
|
||||
}
|
||||
Set<String> normalizedScopes = KnowledgeShareActionScope.normalize(actionScopes);
|
||||
if (normalizedScopes.isEmpty()) {
|
||||
throw new BusinessException("动作范围不能为空");
|
||||
}
|
||||
|
||||
mappingService.removeScopedMappings(apiKeyId, RESOURCE_TYPE_KNOWLEDGE, knowledgeId);
|
||||
List<SysApiKeyResourceMapping> rows = new ArrayList<>();
|
||||
for (String scope : normalizedScopes) {
|
||||
List<String> uris = URI_SCOPE_MAPPING.get(scope);
|
||||
if (uris == null || uris.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (String uri : uris) {
|
||||
SysApiKeyResource resource = ensureResource(uri);
|
||||
SysApiKeyResourceMapping row = new SysApiKeyResourceMapping();
|
||||
row.setApiKeyId(apiKeyId);
|
||||
row.setApiKeyResourceId(resource.getId());
|
||||
row.setResourceType(RESOURCE_TYPE_KNOWLEDGE);
|
||||
row.setResourceTargetId(knowledgeId);
|
||||
row.setActionScope(scope);
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
mappingService.saveBatch(rows);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void replaceApiShareEnabled(BigInteger apiKeyId, boolean enabled) {
|
||||
if (apiKeyId == null) {
|
||||
throw new BusinessException("系统访问令牌不能为空");
|
||||
}
|
||||
SysApiKey apiKey = sysApiKeyService.getById(apiKeyId);
|
||||
if (apiKey == null) {
|
||||
throw new BusinessException("系统访问令牌不存在");
|
||||
}
|
||||
mappingService.removeScopedMappings(apiKeyId, RESOURCE_TYPE_KNOWLEDGE);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysApiKeyResourceMapping> rows = new ArrayList<>();
|
||||
for (String scope : KnowledgeShareActionScope.defaultApiScopes()) {
|
||||
List<String> uris = URI_SCOPE_MAPPING.get(scope);
|
||||
if (uris == null || uris.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (String uri : uris) {
|
||||
SysApiKeyResource resource = ensureResource(uri);
|
||||
SysApiKeyResourceMapping row = new SysApiKeyResourceMapping();
|
||||
row.setApiKeyId(apiKeyId);
|
||||
row.setApiKeyResourceId(resource.getId());
|
||||
row.setResourceType(RESOURCE_TYPE_KNOWLEDGE);
|
||||
row.setActionScope(scope);
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
mappingService.saveBatch(rows);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertApiShare(BigInteger apiKeyId, String requestUri, BigInteger knowledgeId, String actionScope) {
|
||||
if (apiKeyId == null || knowledgeId == null) {
|
||||
throw new BusinessException("API 分享鉴权参数不完整");
|
||||
}
|
||||
sysApiKeyService.checkResourceScope(apiKeyId, requestUri, RESOURCE_TYPE_KNOWLEDGE, knowledgeId, actionScope);
|
||||
}
|
||||
|
||||
private SysApiKeyResource ensureResource(String requestInterface) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(SysApiKeyResource::getRequestInterface, requestInterface);
|
||||
SysApiKeyResource resource = resourceService.getOne(wrapper);
|
||||
if (resource != null) {
|
||||
return resource;
|
||||
}
|
||||
resource = new SysApiKeyResource();
|
||||
resource.setRequestInterface(requestInterface);
|
||||
resource.setTitle("知识库分享接口");
|
||||
resourceService.save(resource);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.constants.KnowledgeShareErrorCode;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareStatus;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareType;
|
||||
import tech.easyflow.ai.mapper.KnowledgeShareMapper;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareUrlCreateResult;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 知识库分享服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeShareServiceImpl extends ServiceImpl<KnowledgeShareMapper, KnowledgeShare> implements KnowledgeShareService {
|
||||
|
||||
private static final Duration DEFAULT_EXPIRE_DURATION = Duration.ofMinutes(30);
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public KnowledgeShareUrlCreateResult createUrlShare(
|
||||
BigInteger knowledgeId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl,
|
||||
Set<String> permissionScopes
|
||||
) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
invalidateExistingUrlShares(knowledgeId, operatorId);
|
||||
|
||||
String shareKey = UUID.randomUUID().toString().replace("-", "");
|
||||
Date now = new Date();
|
||||
Date expiresAt = new Date(now.getTime() + DEFAULT_EXPIRE_DURATION.toMillis());
|
||||
|
||||
KnowledgeShare share = new KnowledgeShare();
|
||||
share.setKnowledgeId(knowledgeId);
|
||||
share.setShareType(KnowledgeShareType.URL.name());
|
||||
share.setShareKeyHash(hashShareKey(shareKey));
|
||||
share.setStatus(KnowledgeShareStatus.ENABLED.name());
|
||||
share.setPermissionScopes(permissionScopes);
|
||||
share.setExpiresAt(expiresAt);
|
||||
share.setTenantId(tenantId);
|
||||
share.setDeptId(deptId);
|
||||
share.setCreated(now);
|
||||
share.setCreatedBy(operatorId);
|
||||
share.setModified(now);
|
||||
share.setModifiedBy(operatorId);
|
||||
save(share);
|
||||
|
||||
KnowledgeShareUrlCreateResult result = new KnowledgeShareUrlCreateResult();
|
||||
result.setId(share.getId());
|
||||
result.setShareKey(shareKey);
|
||||
result.setExpiresAt(expiresAt);
|
||||
result.setShareUrl(buildShareUrl(baseUrl, shareKey));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void invalidateExistingUrlShares(BigInteger knowledgeId, BigInteger operatorId) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(KnowledgeShare::getKnowledgeId, knowledgeId)
|
||||
.eq(KnowledgeShare::getShareType, KnowledgeShareType.URL.name())
|
||||
.eq(KnowledgeShare::getStatus, KnowledgeShareStatus.ENABLED.name());
|
||||
List<KnowledgeShare> activeShares = list(wrapper);
|
||||
if (activeShares == null || activeShares.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
for (KnowledgeShare activeShare : activeShares) {
|
||||
KnowledgeShare update = new KnowledgeShare();
|
||||
update.setId(activeShare.getId());
|
||||
update.setStatus(KnowledgeShareStatus.DISABLED.name());
|
||||
update.setModified(now);
|
||||
update.setModifiedBy(operatorId);
|
||||
updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeShareAuthContext validateUrlShare(String shareKey) {
|
||||
if (shareKey == null || shareKey.isBlank()) {
|
||||
throw invalidShare();
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(KnowledgeShare::getShareKeyHash, hashShareKey(shareKey));
|
||||
KnowledgeShare share = getOne(wrapper);
|
||||
if (share == null) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (KnowledgeShareStatus.REVOKED.name().equals(share.getStatus())
|
||||
|| KnowledgeShareStatus.DISABLED.name().equals(share.getStatus())) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (share.getExpiresAt() == null || share.getExpiresAt().before(new Date())) {
|
||||
throw expiredShare();
|
||||
}
|
||||
DocumentCollection knowledge = documentCollectionService.getById(share.getKnowledgeId());
|
||||
if (knowledge == null) {
|
||||
throw invalidShare();
|
||||
}
|
||||
KnowledgeShareAuthContext context = new KnowledgeShareAuthContext();
|
||||
context.setShare(share);
|
||||
context.setKnowledge(knowledge);
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeShareAuthContext assertUrlShareAccess(
|
||||
String shareKey,
|
||||
BigInteger knowledgeId,
|
||||
String actionScope
|
||||
) {
|
||||
KnowledgeShareAuthContext context = validateUrlShare(shareKey);
|
||||
if (knowledgeId != null && context.getKnowledge() != null
|
||||
&& context.getKnowledge().getId() != null
|
||||
&& context.getKnowledge().getId().compareTo(knowledgeId) != 0) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (actionScope != null && !actionScope.isBlank()
|
||||
&& !context.getShare().getPermissionScopeSet().contains(actionScope.trim().toUpperCase())) {
|
||||
throw forbiddenShare("当前分享不允许执行该操作");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private String buildShareUrl(String baseUrl, String shareKey) {
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return baseUrl + (baseUrl.contains("?") ? "&" : "?")
|
||||
+ "shareKey=" + shareKey;
|
||||
}
|
||||
|
||||
private String hashShareKey(String shareKey) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] value = digest.digest(shareKey.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(value);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private BusinessException expiredShare() {
|
||||
return new BusinessException(KnowledgeShareErrorCode.KNOWLEDGE_SHARE_EXPIRED + ":链接已过期");
|
||||
}
|
||||
|
||||
private BusinessException invalidShare() {
|
||||
return new BusinessException(KnowledgeShareErrorCode.KNOWLEDGE_SHARE_INVALID + ":链接无效");
|
||||
}
|
||||
|
||||
private BusinessException forbiddenShare(String message) {
|
||||
return new BusinessException(KnowledgeShareErrorCode.KNOWLEDGE_SHARE_FORBIDDEN + ":" + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 知识库分享鉴权上下文。
|
||||
*/
|
||||
public class KnowledgeShareAuthContext implements Serializable {
|
||||
|
||||
private KnowledgeShare share;
|
||||
private DocumentCollection knowledge;
|
||||
|
||||
public KnowledgeShare getShare() {
|
||||
return share;
|
||||
}
|
||||
|
||||
public void setShare(KnowledgeShare share) {
|
||||
this.share = share;
|
||||
}
|
||||
|
||||
public DocumentCollection getKnowledge() {
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
public void setKnowledge(DocumentCollection knowledge) {
|
||||
this.knowledge = knowledge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* URL 分享创建结果。
|
||||
*/
|
||||
public class KnowledgeShareUrlCreateResult implements Serializable {
|
||||
|
||||
private BigInteger id;
|
||||
private String shareKey;
|
||||
private String shareUrl;
|
||||
private Date expiresAt;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getShareKey() {
|
||||
return shareKey;
|
||||
}
|
||||
|
||||
public void setShareKey(String shareKey) {
|
||||
this.shareKey = shareKey;
|
||||
}
|
||||
|
||||
public String getShareUrl() {
|
||||
return shareUrl;
|
||||
}
|
||||
|
||||
public void setShareUrl(String shareUrl) {
|
||||
this.shareUrl = shareUrl;
|
||||
}
|
||||
|
||||
public Date getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Date expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,20 @@ public class CurdInterceptor implements HandlerInterceptor {
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
String contextPath = request.getContextPath();
|
||||
String normalizedRequestUri = requestURI;
|
||||
if (StrUtil.isNotBlank(contextPath) && StrUtil.startWith(requestURI, contextPath)) {
|
||||
normalizedRequestUri = requestURI.substring(contextPath.length());
|
||||
}
|
||||
|
||||
log.info("进入 CurdInterceptor requestURI:{}", requestURI);
|
||||
|
||||
// 分享访问接口与公开 API 走各自的业务鉴权,不走后台通用 CRUD 权限码拦截。
|
||||
if (StrUtil.startWith(normalizedRequestUri, "/api/v1/share/")
|
||||
|| StrUtil.startWith(normalizedRequestUri, "/public-api/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String groupName = "";
|
||||
// 检查handler是否是HandlerMethod类型
|
||||
if (handler instanceof HandlerMethod) {
|
||||
|
||||
@@ -23,6 +23,9 @@ public class SysApiKey extends SysApiKeyBase {
|
||||
@Column(ignore = true)
|
||||
List<BigInteger> permissionIds;
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean knowledgeShareEnabled;
|
||||
|
||||
@RelationOneToMany(selfField = "id", targetField = "apiKeyId", targetTable = "tb_sys_api_key_resource_mapping")
|
||||
private List<SysApiKeyResourceMapping> resourcePermissions;
|
||||
|
||||
@@ -41,4 +44,12 @@ public class SysApiKey extends SysApiKeyBase {
|
||||
public void setPermissionIds(List<BigInteger> permissionIds) {
|
||||
this.permissionIds = permissionIds;
|
||||
}
|
||||
|
||||
public Boolean getKnowledgeShareEnabled() {
|
||||
return knowledgeShareEnabled;
|
||||
}
|
||||
|
||||
public void setKnowledgeShareEnabled(Boolean knowledgeShareEnabled) {
|
||||
this.knowledgeShareEnabled = knowledgeShareEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,24 @@ public class SysApiKeyResourceMappingBase implements Serializable {
|
||||
@Column(comment = "请求接口资源访问id")
|
||||
private BigInteger apiKeyResourceId;
|
||||
|
||||
/**
|
||||
* 资源类型
|
||||
*/
|
||||
@Column(comment = "资源类型")
|
||||
private String resourceType;
|
||||
|
||||
/**
|
||||
* 资源目标ID
|
||||
*/
|
||||
@Column(comment = "资源目标ID")
|
||||
private BigInteger resourceTargetId;
|
||||
|
||||
/**
|
||||
* 动作范围
|
||||
*/
|
||||
@Column(comment = "动作范围")
|
||||
private String actionScope;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -53,4 +71,28 @@ public class SysApiKeyResourceMappingBase implements Serializable {
|
||||
this.apiKeyResourceId = apiKeyResourceId;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String resourceType) {
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public BigInteger getResourceTargetId() {
|
||||
return resourceTargetId;
|
||||
}
|
||||
|
||||
public void setResourceTargetId(BigInteger resourceTargetId) {
|
||||
this.resourceTargetId = resourceTargetId;
|
||||
}
|
||||
|
||||
public String getActionScope() {
|
||||
return actionScope;
|
||||
}
|
||||
|
||||
public void setActionScope(String actionScope) {
|
||||
this.actionScope = actionScope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* apikey-请求接口表 服务层。
|
||||
*
|
||||
@@ -13,4 +15,21 @@ import tech.easyflow.system.entity.SysApiKeyResourceMapping;
|
||||
public interface SysApiKeyResourceMappingService extends IService<SysApiKeyResourceMapping> {
|
||||
|
||||
void authInterface(SysApiKey entity);
|
||||
|
||||
/**
|
||||
* 移除指定 apiKey 下某个资源目标的 scope 授权。
|
||||
*
|
||||
* @param apiKeyId apiKey ID
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceTargetId 资源目标 ID
|
||||
*/
|
||||
void removeScopedMappings(BigInteger apiKeyId, String resourceType, BigInteger resourceTargetId);
|
||||
|
||||
/**
|
||||
* 按资源类型移除指定 apiKey 下的全部资源级授权。
|
||||
*
|
||||
* @param apiKeyId apiKey ID
|
||||
* @param resourceType 资源类型
|
||||
*/
|
||||
void removeScopedMappings(BigInteger apiKeyId, String resourceType);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package tech.easyflow.system.service;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 服务层。
|
||||
*
|
||||
@@ -15,4 +17,15 @@ public interface SysApiKeyService extends IService<SysApiKey> {
|
||||
|
||||
SysApiKey getSysApiKey(String apiKey);
|
||||
|
||||
/**
|
||||
* 校验资源级作用域权限。
|
||||
*
|
||||
* @param apiKeyId apiKey 记录 ID
|
||||
* @param requestURI 请求 URI
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceTargetId 资源目标 ID
|
||||
* @param actionScope 动作范围
|
||||
*/
|
||||
void checkResourceScope(BigInteger apiKeyId, String requestURI, String resourceType, BigInteger resourceTargetId, String actionScope);
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl<SysApiKeyRe
|
||||
return;
|
||||
}
|
||||
redisLockExecutor.executeWithLock(API_KEY_MAPPING_LOCK_PREFIX + apiKeyId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
|
||||
this.remove(QueryWrapper.create().eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId));
|
||||
QueryWrapper removeWrapper = QueryWrapper.create()
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
|
||||
.isNull(SysApiKeyResourceMapping::getResourceType);
|
||||
this.remove(removeWrapper);
|
||||
if (entity.getPermissionIds() == null || entity.getPermissionIds().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -66,4 +69,29 @@ public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl<SysApiKeyRe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeScopedMappings(BigInteger apiKeyId, String resourceType, BigInteger resourceTargetId) {
|
||||
if (apiKeyId == null || resourceTargetId == null || resourceType == null || resourceType.isBlank()) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
|
||||
.eq(SysApiKeyResourceMapping::getResourceType, resourceType)
|
||||
.eq(SysApiKeyResourceMapping::getResourceTargetId, resourceTargetId);
|
||||
remove(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeScopedMappings(BigInteger apiKeyId, String resourceType) {
|
||||
if (apiKeyId == null || resourceType == null || resourceType.isBlank()) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
|
||||
.eq(SysApiKeyResourceMapping::getResourceType, resourceType);
|
||||
remove(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,4 +79,36 @@ public class SysApiKeyServiceImpl extends ServiceImpl<SysApiKeyMapper, SysApiKey
|
||||
return one;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkResourceScope(BigInteger apiKeyId, String requestURI, String resourceType, BigInteger resourceTargetId, String actionScope) {
|
||||
List<String> candidateRequestUris = getCandidateRequestUris(requestURI);
|
||||
QueryWrapper resourceWrapper = QueryWrapper.create();
|
||||
resourceWrapper.in(SysApiKeyResource::getRequestInterface, candidateRequestUris);
|
||||
List<SysApiKeyResource> resources = resourceService.list(resourceWrapper);
|
||||
if (resources == null || resources.isEmpty()) {
|
||||
throw new BusinessException("该接口不存在");
|
||||
}
|
||||
List<BigInteger> resourceIds = resources.stream()
|
||||
.map(SysApiKeyResource::getId)
|
||||
.toList();
|
||||
QueryWrapper exactScopeWrapper = QueryWrapper.create()
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
|
||||
.in(SysApiKeyResourceMapping::getApiKeyResourceId, resourceIds)
|
||||
.eq(SysApiKeyResourceMapping::getResourceType, resourceType)
|
||||
.eq(SysApiKeyResourceMapping::getResourceTargetId, resourceTargetId)
|
||||
.eq(SysApiKeyResourceMapping::getActionScope, actionScope);
|
||||
if (mappingService.count(exactScopeWrapper) > 0) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper globalScopeWrapper = QueryWrapper.create();
|
||||
globalScopeWrapper.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId);
|
||||
globalScopeWrapper.in(SysApiKeyResourceMapping::getApiKeyResourceId, resourceIds);
|
||||
globalScopeWrapper.eq(SysApiKeyResourceMapping::getResourceType, resourceType);
|
||||
globalScopeWrapper.isNull(SysApiKeyResourceMapping::getResourceTargetId);
|
||||
globalScopeWrapper.eq(SysApiKeyResourceMapping::getActionScope, actionScope);
|
||||
if (mappingService.count(globalScopeWrapper) == 0) {
|
||||
throw new BusinessException("该apiKey无权限访问当前资源");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user