diff --git a/backend/ruoyi-modules/ruoyi-aihr/pom.xml b/backend/ruoyi-modules/ruoyi-aihr/pom.xml
index a5cf11e1..c65c4edf 100644
--- a/backend/ruoyi-modules/ruoyi-aihr/pom.xml
+++ b/backend/ruoyi-modules/ruoyi-aihr/pom.xml
@@ -31,6 +31,11 @@
ruoyi-common-web
+
+ org.dromara
+ ruoyi-common-redis
+
+
org.dromara
ruoyi-system
diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgeAppDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgeAppDto.java
new file mode 100644
index 00000000..c5ff7b04
--- /dev/null
+++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgeAppDto.java
@@ -0,0 +1,37 @@
+package org.dromara.aihr.knowledge.domain;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public final class AihrKnowledgeAppDto {
+
+ private AihrKnowledgeAppDto() {
+ }
+
+ public record AuthenticatedApp(
+ Long id,
+ String tenantId,
+ String appCode,
+ String appName,
+ String authType,
+ Integer rateLimitPerMinute,
+ LocalDateTime expiresTime
+ ) {
+ }
+
+ public record AppResponse(
+ Long id,
+ String appCode,
+ String appName,
+ String authType,
+ String internalClientKey,
+ String status,
+ Integer rateLimitPerMinute,
+ LocalDateTime expiresTime,
+ List spaceCodes
+ ) {
+ }
+
+ public record TokenIssuedResponse(String appCode, String plainToken, LocalDateTime expiresTime) {
+ }
+}
diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgePrincipal.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgePrincipal.java
new file mode 100644
index 00000000..27b0689c
--- /dev/null
+++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgePrincipal.java
@@ -0,0 +1,18 @@
+package org.dromara.aihr.knowledge.domain;
+
+import java.util.Set;
+
+public record AihrKnowledgePrincipal(
+ String tenantId,
+ Long userId,
+ String userType,
+ String extPartyId,
+ Set roles,
+ Set projectCodes,
+ String clientKey
+) {
+ public AihrKnowledgePrincipal {
+ roles = roles == null ? Set.of() : Set.copyOf(roles);
+ projectCodes = projectCodes == null ? Set.of() : Set.copyOf(projectCodes);
+ }
+}
diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAccessService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAccessService.java
new file mode 100644
index 00000000..778fa390
--- /dev/null
+++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAccessService.java
@@ -0,0 +1,135 @@
+package org.dromara.aihr.knowledge.service;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.aihr.knowledge.domain.AihrKnowledgeAppDto.AuthenticatedApp;
+import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
+import org.dromara.common.core.constant.HttpStatus;
+import org.dromara.common.core.exception.ServiceException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+@RequiredArgsConstructor
+public class AihrKnowledgeAccessService {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public Set resolveInternalSpaceIds(AihrKnowledgePrincipal principal, AuthenticatedApp app,
+ List requestedCodes, String permission) {
+ requireSameTenant(principal.tenantId(), app.tenantId());
+ Set appSpaces = appSpaces(app);
+ Set principalSpaces = principalSpaces(principal, permission);
+ Set requestedSpaces = requestedSpaceIds(principal.tenantId(), requestedCodes);
+ return intersect(appSpaces, principalSpaces, requestedSpaces);
+ }
+
+ public Set resolveExternalSpaceIds(AuthenticatedApp app, List requestedCodes) {
+ Set appSpaces = appSpaces(app);
+ Set requestedSpaces = requestedSpaceIds(app.tenantId(), requestedCodes);
+ return narrow(appSpaces, requestedSpaces);
+ }
+
+ public static Set intersect(Set appSpaces, Set principalSpaces, Set requestedSpaces) {
+ Set effective = new LinkedHashSet<>(appSpaces);
+ effective.retainAll(principalSpaces);
+ return narrow(effective, requestedSpaces);
+ }
+
+ private static Set narrow(Set effective, Set requested) {
+ if (effective.isEmpty()) {
+ throw forbidden("当前应用和账号没有共同可访问的知识空间");
+ }
+ if (requested == null || requested.isEmpty()) {
+ return Set.copyOf(effective);
+ }
+ if (!effective.containsAll(requested)) {
+ throw forbidden("请求包含未授权的知识空间");
+ }
+ return Set.copyOf(requested);
+ }
+
+ private Set appSpaces(AuthenticatedApp app) {
+ return new LinkedHashSet<>(jdbcTemplate.queryForList("""
+ select s.knowledge_id
+ from aihr_knowledge_app_space s
+ join aihr_knowledge_info k on k.id = s.knowledge_id and k.tenant_id = s.tenant_id
+ where s.tenant_id = ? and s.app_id = ? and k.status = 'ACTIVE'
+ """, Long.class, app.tenantId(), app.id()));
+ }
+
+ private Set principalSpaces(AihrKnowledgePrincipal principal, String permission) {
+ List