第17章 后台管理端与演示支持

本章目标

上一章我们完成了 Vue 3 用户端前端实战。

用户端解决的是普通用户怎么使用系统:

text 复制代码
登录
-> 创建知识库
-> 上传文档
-> 等待索引
-> 向量检索
-> RAG 问答
-> 查看引用和模型状态

但是,一个企业级平台不能只有用户端。

因为普通用户只能看到自己的数据,而系统维护者需要知道整个平台的运行情况:

  • 当前有哪些用户。
  • 哪些用户是管理员。
  • 哪些知识库被创建了。
  • 哪些文档索引成功或失败。
  • 哪些索引任务一直卡住。
  • 哪些问答触发了 AI 降级。
  • 项目演示时,应该怎么快速展示完整能力。

这些事情就需要后台管理端。

在 KnowHub 中,后台管理端对应 rag-admin-web,后端接口统一使用 /admin/** 路径,并由 Gateway 校验管理员权限。

本章要讲清楚四件事:

  1. 后台管理端为什么不是用户端的重复。
  2. USER 和 ADMIN 这套基础 RBAC 是怎么落地的。
  3. rag-admin-web 如何组织登录、用户、知识库、文档、任务和问答日志视图。
  4. 管理端如何支撑项目演示和问题排查。

17.1 为什么需要后台管理端

先从一个简单问题开始:

text 复制代码
普通用户能看到自己的知识库,为什么还要做管理端?

原因很现实。

普通用户关注的是"我的业务能不能完成"。管理员关注的是"整个平台是否健康"。

两者视角不同。

普通用户进入用户端后,看到的是:

  • 我的知识库
  • 我的文档
  • 我的问答
  • 我的任务状态

管理员进入管理端后,看到的是:

  • 全部用户
  • 全部知识库
  • 全部文档
  • 全部索引任务
  • 全部问答日志

也就是说,用户端是个人工作台,管理端是平台观察台。

在 RAG 平台里,管理端尤其重要。因为 RAG 系统的链路比普通 CRUD 系统更长:

text 复制代码
登录认证
-> 文件上传
-> 文档解析
-> 文本切片
-> 异步任务
-> 向量化
-> pgvector 写入
-> 检索召回
-> 模型生成
-> 限流降级
-> 日志记录

任何一个环节出问题,普通用户通常只能看到"失败"或"没有答案"。管理员则需要根据后台数据判断问题在哪里。

所以管理端不是可有可无的页面,而是平台治理、排查和演示的重要入口。


17.2 后台管理端和用户端的区别

第 16 章的 rag-user-web 面向普通用户。

第 17 章的 rag-admin-web 面向管理员。

它们都用 Vue 3 和 Element Plus,但目标完全不同。

用户端强调的是流程:

text 复制代码
让用户完成知识库问答。

管理端强调的是观察和控制:

text 复制代码
让管理员看到系统全局状态,并对关键对象进行处理。

可以用下面这张对照表理解。

对比项 用户端 管理端
使用者 普通用户 管理员
数据范围 当前用户自己的资源 全局资源
核心页面 知识库工作台 后台控制台
核心操作 上传、检索、问答 查询、过滤、角色调整、任务重试
权限要求 登录即可 必须是 ADMIN
主要价值 完成业务流程 排查、治理、演示

这里要强调一个原则:

text 复制代码
管理端权限不能只靠前端判断。

前端可以判断当前登录用户是不是 ADMIN,但这只是为了改善体验。真正的权限边界必须在 Gateway 和后端。

否则,用户只要绕过前端,直接请求 /admin/** 接口,就可能访问管理员数据。

KnowHub 的做法是:

text 复制代码
前端检查 ADMIN -> Gateway 再检查 ADMIN -> 后端只暴露 /admin/** 管理接口

这样权限边界更可靠。


17.3 基础 RBAC:USER 与 ADMIN

RBAC 是 Role-Based Access Control 的缩写,意思是"基于角色的访问控制"。

不要被这个词吓到。

在 KnowHub 当前阶段,RBAC 很简单,只区分两个角色:

  • USER:普通用户。
  • ADMIN:管理员。

角色定义在 rag-commonUserRole 中:

java 复制代码
public enum UserRole {
    USER,
    ADMIN;
}

当前系统没有做复杂的"菜单权限、按钮权限、数据权限规则引擎"。这是刻意控制范围。

对于一个学习型企业 RAG 项目,先把两个问题做好更重要:

  1. 普通用户不能访问后台接口。
  2. 管理员可以查看全局数据。

role 字段从哪里来

数据库里通过脚本给 user_account 表增加 role 字段。

脚本位置是:

text 复制代码
sql/mysql/02_admin_rbac_schema.sql

核心逻辑是:

sql 复制代码
ALTER TABLE user_account
  ADD COLUMN role VARCHAR(16) NOT NULL DEFAULT 'USER'
  COMMENT '用户角色:USER=普通用户,ADMIN=管理员'
  AFTER nickname;

默认注册出来的用户都是 USER

如果要创建管理员,当前项目采用一个很适合教学演示的做法:

sql 复制代码
UPDATE user_account SET role = 'ADMIN' WHERE username = '你的管理员用户名';

也就是说,先正常注册账号,再手动把这个账号升级为管理员。

这种方式虽然不是完整商业系统里的管理员开通流程,但非常适合学习和演示,因为它简单、可控、不会把管理员密码写进 SQL。


17.4 JWT 中的 role 如何被 Gateway 使用

有了数据库里的 role 字段还不够。

前端访问后台接口时,Gateway 不会每次都去查数据库,它主要看 Token。

所以登录成功签发 JWT 时,必须把角色写进 Token。

JwtTokenService 里有一个 ROLE_CLAIM

java 复制代码
private static final String ROLE_CLAIM = "role";

签发 Token 时,会写入:

java 复制代码
.claim(ROLE_CLAIM, UserRole.normalize(role))

解析 Token 时,又会把它取出来形成 UserClaims

这样 Gateway 就知道当前请求者是 USER 还是 ADMIN

Gateway 怎么挡住普通用户

Gateway 的全局过滤器是 JwtAuthGlobalFilter

它里面有一个后台路径前缀:

java 复制代码
private static final String ADMIN_PATH_PREFIX = "/admin/";

如果请求路径是 /admin/ 开头,但 Token 中的角色不是 ADMIN,就会抛出异常:

java 复制代码
throw new BusinessException(ErrorCode.FORBIDDEN, "只有管理员可以访问后台接口");

这一步非常关键。

因为它说明管理端权限不是页面自己说了算,而是由统一网关负责拦截。

Gateway 还会覆盖用户头

过滤器里还有一个很重要的安全细节:

text 复制代码
覆盖外部伪造的用户头,只信任 JWT 解析出的用户身份。

也就是说,即使有人自己构造请求头:

text 复制代码
X-User-Id: 1
X-User-Role: ADMIN

Gateway 也不会直接相信,而是先删掉外部传入的头,再根据 JWT 重新写入可信用户信息。

这就是企业级系统必须具备的安全意识。


17.4.4 JwtAuthGlobalFilter 管理端权限校验逻辑

下面给出 JwtAuthGlobalFilter 中和管理端权限相关的核心代码片段。

这段逻辑通常放在 Token 已经校验通过之后、向下游服务透传 X-User-IdX-User-Role 之前。也就是说,Gateway 先确认"你是谁",再确认"你是不是管理员",最后才把可信身份传给后端服务。

java 复制代码
private static final String ADMIN_PATH_PREFIX = "/admin/";
private static final String ROLE_ADMIN = "ADMIN";

private Mono<Void> filterAdminRequest(ServerWebExchange exchange,
                                      GatewayFilterChain chain,
                                      UserClaims claims) {
    String path = exchange.getRequest().getURI().getPath();

    // 只有 `/admin/**` 需要进入管理员校验;普通用户端接口继续走原有登录校验。
    if (!path.startsWith(ADMIN_PATH_PREFIX)) {
        return chain.filter(exchange);
    }

    // claims 来自已经校验通过的 JWT,不信任前端自己传入的 `X-User-Role`。
    String role = claims.getRole();
    if (!ROLE_ADMIN.equalsIgnoreCase(role)) {
        throw new BusinessException(
                ErrorCode.FORBIDDEN,
                "只有管理员可以访问后台接口"
        );
    }

    // 管理员校验通过后,再进入后续的用户信息透传逻辑。
    return chain.filter(exchange);
}

这里的关键点不是代码长短,而是边界位置:

text 复制代码
JWT 校验通过 -> 判断 `/admin/**` 是否要求 ADMIN -> 覆盖并透传用户身份 -> 路由到后端服务

这样即使普通用户绕过管理端页面,直接调用 /admin/users,也会被 Gateway 拦住。


17.5 Gateway 后台路由怎么分发

管理端前端只访问 Gateway,不直接访问具体服务。

Gateway 再把不同 /admin/** 路径转发到不同微服务。

rag-gateway-serviceapplication.yml 中,后台路由分成三组。

用户管理路由

text 复制代码
/admin/users/** -> rag-auth-service

用户账号、角色、状态都属于认证服务,所以用户管理接口放在 auth-service。

知识库管理路由

text 复制代码
/admin/knowledge-bases/**
/admin/documents/**
/admin/qa-logs/**
-> rag-knowledge-service

知识库、文档和问答日志都是知识库服务的数据,所以它们进入 knowledge-service。

索引任务路由

text 复制代码
/admin/index-tasks/** -> rag-task-service

索引任务的执行和重试属于 task-service,所以后台任务管理接口转发到 task-service。

这和前面微服务拆分章节是一致的:

text 复制代码
auth-service 管用户
knowledge-service 管知识库和问答
task-service 管索引任务
Gateway 管统一入口和鉴权

17.5.4 Gateway 后台路由完整配置

下面给出 rag-gateway-service/src/main/resources/application.yml 中后台路由的完整配置示例。

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: admin-user-routes
          uri: lb://auth-service
          predicates:
            - Path=/admin/users/**
          filters:
            # 去掉 /admin 前缀,auth-service 实际收到的是 /users/**
            - StripPrefix=1

        - id: admin-kb-doc-routes
          uri: lb://knowledge-service
          predicates:
            - Path=/admin/knowledge-bases/**,/admin/documents/**
          filters:
            # 去掉 /admin 前缀,knowledge-service 实际收到的是 /knowledge-bases/** 或 /documents/**
            - StripPrefix=1

        - id: admin-task-log-routes
          uri: lb://task-service
          predicates:
            - Path=/admin/index-tasks/**,/admin/qa-logs/**
          filters:
            # 去掉 /admin 前缀,task-service 实际收到的是 /index-tasks/** 或 /qa-logs/**
            - StripPrefix=1

StripPrefix=1 的作用是去掉路径中的第一段 /admin

例如前端请求:

text 复制代码
GET /admin/users

经过 Gateway 后,auth-service 实际收到的是:

text 复制代码
GET /users

所以后端管理控制器上的 @RequestMapping 不需要再写 /admin 前缀。/admin 是 Gateway 层的统一入口语义,后端服务只关心自己的资源路径。


17.6 rag-admin-web 项目结构

rag-admin-web 是一个 Vue 3 管理端前端。

它的文件比用户端更集中。

核心文件有四个:

  • src/main.ts
  • src/api.ts
  • src/App.vue
  • src/styles.css

虽然 package.json 里也安装了 Pinia 和 Vue Router,但当前管理端实现主要是一个单页控制台,没有像用户端那样拆出多个路由页面和 store。

这点要如实理解。

main.ts

入口非常简单:

ts 复制代码
createApp(App).use(ElementPlus).mount('#app')

这说明当前管理端只注册了 ElementPlus,没有额外注册 Router 或 Pinia。

api.ts

这个文件比较重要,它承担了四类职责:

  1. 保存管理员 Token。
  2. 创建 Axios 请求实例。
  3. 定义接口返回类型。
  4. 封装后台接口函数。

管理端 Token 存储 key 是:

ts 复制代码
const TOKEN_KEY = 'rag_admin_token'

这和用户端的 rag_user_token 分开,避免两个前端登录态互相影响。

管理端默认后端地址是:

ts 复制代码
const BASE_URL = import.meta.env.VITE_RAG_GATEWAY_URL || 'http://localhost:9000'

这说明它也是默认访问 Gateway 的 9000 端口。

App.vue

当前所有管理端页面逻辑都集中在 App.vue

它包含:

  • 管理员登录页。
  • 左侧导航。
  • 顶部栏。
  • 用户管理视图。
  • 知识库管理视图。
  • 文档管理视图。
  • 索引任务视图。
  • 问答日志视图。

这种写法不是大型后台系统的最终形态,但对于当前项目演示非常实用。

因为它减少了文件数量,让读者能在一个组件里看完整后台控制台的基本逻辑。


17.6.6 管理员接口示例:AdminUserController

下面给出 auth-service 中 AdminUserController 的完整示例。

注意这里的路径是 /users,不是 /admin/users。因为 Gateway 已经通过 StripPrefix=1 去掉了 /admin 前缀。

java 复制代码
package com.knowhub.auth.admin;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.knowhub.auth.entity.UserAccount;
import com.knowhub.auth.service.UserAccountService;
import com.knowhub.common.context.UserContext;
import com.knowhub.common.exception.BusinessException;
import com.knowhub.common.exception.ErrorCode;
import com.knowhub.common.response.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class AdminUserController {

    private final UserAccountService userAccountService;

    @GetMapping
    public ApiResponse<List<UserAccount>> listUsers(@RequestParam(required = false) String username,
                                                    @RequestParam(required = false) String role) {
        requireAdmin();

        LambdaQueryWrapper<UserAccount> wrapper = new LambdaQueryWrapper<>();
        // 管理端查询全局用户列表,不带 userId 过滤;这和用户端"只查当前用户资源"不同。
        wrapper.like(StringUtils.hasText(username), UserAccount::getUsername, username)
                .eq(StringUtils.hasText(role), UserAccount::getRole, role)
                .orderByDesc(UserAccount::getCreatedAt);

        return ApiResponse.success(userAccountService.list(wrapper));
    }

    @PutMapping("/{userId}/role")
    public ApiResponse<Void> updateRole(@PathVariable Long userId,
                                        @RequestParam String role) {
        requireAdmin();
        if (!"USER".equals(role) && !"ADMIN".equals(role)) {
            throw new BusinessException(ErrorCode.PARAM_ERROR, "role 只能是 USER 或 ADMIN");
        }

        UserAccount entity = new UserAccount();
        entity.setId(userId);
        entity.setRole(role);
        userAccountService.updateById(entity);
        return ApiResponse.success(null);
    }

    @PutMapping("/{userId}/status")
    public ApiResponse<Void> updateStatus(@PathVariable Long userId,
                                          @RequestParam Boolean enabled) {
        requireAdmin();

        UserAccount entity = new UserAccount();
        entity.setId(userId);
        entity.setEnabled(Boolean.TRUE.equals(enabled));
        userAccountService.updateById(entity);
        return ApiResponse.success(null);
    }

    private void requireAdmin() {
        // Gateway 已经做过 ADMIN 校验,这里保留二次防御,避免服务被绕过直连。
        if (!"ADMIN".equalsIgnoreCase(UserContext.requireRole())) {
            throw new BusinessException(ErrorCode.FORBIDDEN, "只有管理员可以访问后台接口");
        }
    }
}

用户端的知识库列表接口通常会带上:

sql 复制代码
WHERE user_id = currentUserId

而管理端的 GET /admin/users 面向全局观察,不按当前用户过滤。这就是管理端接口和用户端接口最根本的区别。


17.6.7 管理员接口示例:AdminIndexTaskController

下面给出 task-serviceAdminIndexTaskController 的关键方法代码。

java 复制代码
package com.knowhub.task.admin;

import com.knowhub.common.context.UserContext;
import com.knowhub.common.exception.BusinessException;
import com.knowhub.common.exception.ErrorCode;
import com.knowhub.common.response.ApiResponse;
import com.knowhub.task.model.response.IndexTaskResponse;
import com.knowhub.task.service.IndexTaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/index-tasks")
@RequiredArgsConstructor
public class AdminIndexTaskController {

    private final IndexTaskService indexTaskService;

    @GetMapping
    public ApiResponse<List<IndexTaskResponse>> listTasks(@RequestParam(required = false) String status,
                                                          @RequestParam(required = false) Long userId,
                                                          @RequestParam(required = false) Long kbId) {
        requireAdmin();
        // 管理端查询全局索引任务,不带 WHERE user_id = currentUserId。
        return ApiResponse.success(indexTaskService.listAdminTasks(status, userId, kbId));
    }

    @PostMapping("/{taskId}/retry")
    public ApiResponse<Void> retry(@PathVariable Long taskId) {
        requireAdmin();
        // 对应第 9 章的索引任务可靠性设计:只有 FAILED/TIMEOUT 且未超过最大重试次数的任务允许重试。
        indexTaskService.retryFailedTask(taskId);
        return ApiResponse.success(null);
    }

    private void requireAdmin() {
        if (!"ADMIN".equalsIgnoreCase(UserContext.requireRole())) {
            throw new BusinessException(ErrorCode.FORBIDDEN, "只有管理员可以访问后台接口");
        }
    }
}

真正的状态校验应该放在 IndexTaskService.retryFailedTask(taskId) 中完成:

text 复制代码
`status` 必须是 `FAILED` 或 `TIMEOUT`
`retry_count` 必须小于 `max_retry_count`
重新投递 `RabbitMQ` 索引消息

这样即使未来有多个入口都能触发重试,也不会绕过任务状态机。


17.7 管理端登录流程

17.7.1 如何启动管理端前端

管理端前端的启动方式和第 16 章的用户端类似。

前置要求:

  • Node.js 16+
  • npm

进入 rag-admin-web 目录:

bash 复制代码
cd rag-admin-web

首次运行先安装依赖:

bash 复制代码
npm install

启动开发服务器:

bash 复制代码
npm run dev

启动成功后,Vite 会在控制台输出本地访问地址。管理端通常运行在:

text 复制代码
http://localhost:5174

如果用户端 rag-user-web 已经占用了 5173,Vite 会自动分配下一个可用端口。实际访问地址以控制台输出为准。

管理端也需要通过 Gateway 调用后端接口。可以在 .env.development 中配置:

env 复制代码
VITE_API_BASE_URL=http://localhost:9000

如果 Gateway 端口不是 9000,这里要同步修改。

管理端和用户端可以同时启动:

text 复制代码
用户端:http://localhost:5173
管理端:http://localhost:5174
后端统一入口:http://localhost:9000

两端都不直接访问具体微服务,而是统一经过 Gateway。这样才能让 JWT 校验、ADMIN 权限判断和路由转发保持一致。


管理端登录仍然调用普通登录接口:

text 复制代码
/auth/login

为什么不单独做一个 /admin/login

因为登录本质上都是认证行为,由 auth-service 统一负责就够了。

区别在于:登录成功后,管理端前端会检查返回用户是不是 ADMIN。

App.vue 中,登录成功后有这样的判断:

ts 复制代码
if (result.role !== 'ADMIN') {
  tokenStore.clear()
  ElMessage.error('当前账号不是管理员')
  return
}

这一步是前端体验层面的拦截。

如果普通用户用管理端登录,即使用户名密码正确,也不会进入后台页面。

但是要注意:这不是最终安全边界。

真正的安全边界仍然是 Gateway 对 /admin/** 的 ADMIN 校验。

刷新页面后怎么恢复登录

管理端启动后会执行:

ts 复制代码
onMounted(loadCurrentUser)

如果本地有 rag_admin_token,就调用:

text 复制代码
/auth/me

后端返回当前用户信息后,再检查 role 是否是 ADMIN。

如果不是 ADMIN,就清理 Token。

这个流程和用户端相似,但管理端多了一步角色校验。


17.7.3 App.vue 核心结构

当前管理端采用"单页多 Tab"的组织方式,而不是拆成多个路由页面。

原因很简单:本章管理端的目标是全局观察和演示支撑,页面数量少,交互链路短,使用一个 App.vue 能让读者更快看到完整控制台是如何组织数据的。

vue 复制代码
<template>
  <el-container class="admin-layout">
    <el-header class="admin-header">
      <div class="brand">KnowHub 管理端</div>
      <div class="user-area">
        <span>{{ currentUsername }}</span>
        <el-button type="danger" plain size="small" @click="logout">退出</el-button>
      </div>
    </el-header>

    <el-main>
      <el-tabs v-model="activeTab">
        <el-tab-pane label="用户管理" name="users">
          <el-table :data="users" border>
            <el-table-column prop="id" label="用户ID" width="100" />
            <el-table-column prop="username" label="用户名" />
            <el-table-column prop="role" label="角色" />
            <el-table-column prop="enabled" label="状态" />
          </el-table>
        </el-tab-pane>

        <el-tab-pane label="知识库" name="knowledgeBases">
          <el-table :data="knowledgeBases" border>
            <el-table-column prop="id" label="知识库ID" />
            <el-table-column prop="userId" label="用户ID" />
            <el-table-column prop="name" label="名称" />
            <el-table-column prop="status" label="状态" />
          </el-table>
        </el-tab-pane>

        <el-tab-pane label="文档" name="documents">
          <el-table :data="documents" border>
            <el-table-column prop="id" label="文档ID" />
            <el-table-column prop="kbId" label="知识库ID" />
            <el-table-column prop="fileName" label="文件名" />
            <el-table-column prop="indexStatus" label="索引状态" />
          </el-table>
        </el-tab-pane>

        <el-tab-pane label="任务" name="tasks">
          <admin-tasks :tasks="tasks" />
        </el-tab-pane>

        <el-tab-pane label="日志" name="logs">
          <el-table :data="logs" border>
            <el-table-column prop="id" label="日志ID" />
            <el-table-column prop="userId" label="用户ID" />
            <el-table-column prop="question" label="问题" />
            <el-table-column prop="modelFallback" label="是否降级" />
            <el-table-column prop="costTimeMs" label="总耗时(ms)" />
          </el-table>
        </el-tab-pane>
      </el-tabs>
    </el-main>
  </el-container>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { removeToken } from './utils/token'
import AdminTasks from './views/AdminTasks.vue'

const router = useRouter()
const activeTab = ref('users')
const currentUsername = ref('admin')

// 五个全局数据数组分别对应管理端的五个观察视图。
const users = ref([])
const knowledgeBases = ref([])
const documents = ref([])
const tasks = ref([])
const logs = ref([])

function logout() {
  removeToken()
  router.push('/login')
}
</script>

如果后续管理端页面变多,再拆成 views/UserManage.vueviews/TaskManage.vue 和路由表会更合适。当前阶段先用一个控制台把全局数据讲清楚。


17.8 用户管理视图

用户管理是后台管理端的第一个视图。

它对应后端接口:

text 复制代码
GET /admin/users
PUT /admin/users/{userId}/role
PUT /admin/users/{userId}/status

后端控制器是:

text 复制代码
AdminUserController

用户列表查询

用户列表支持三个过滤条件:

  • keyword:用户名或昵称。
  • role:USER 或 ADMIN。
  • status:正常或禁用。

后端查询时限制最多返回 100 条:

text 复制代码
LIMIT 100

这是一种很简单的保护手段,避免后台页面一次拉出过多数据。

修改角色

页面允许把用户角色改为:

  • USER
  • ADMIN

后端并不是直接相信前端传来的字符串,而是调用 normalizeStrictRole 校验。

如果角色不是 USER 或 ADMIN,就返回参数错误。

这说明后端仍然要做校验,不能依赖前端下拉框。

启用和禁用用户

用户状态只能是:

  • 1:正常
  • 0:禁用

请求 DTO 使用了 @Min(0)@Max(1)

这类校验很基础,但非常有必要。因为后台管理端修改的是平台级数据,不能随意接受非法状态值。


17.9 知识库管理视图

知识库管理视图对应:

text 复制代码
GET /admin/knowledge-bases

它由 knowledge-service 的 AdminKnowledgeController 提供。

支持过滤条件:

  • userId
  • status
  • keyword

这里最重要的差异是数据范围。

用户端的知识库列表只能看当前用户自己的知识库。

管理端可以按用户 ID 查看全局知识库。

这就是"用户端"和"管理端"的本质区别。

管理端知识库表格展示:

  • 知识库 ID
  • 用户 ID
  • 名称
  • 描述
  • 状态
  • 创建时间

这对演示很有帮助。

比如你可以先用普通用户创建知识库,然后切到管理端,立即看到这条知识库记录出现。

这样老师、HR 或面试官就能直观看到:

text 复制代码
普通用户操作产生的数据,可以被管理员从全局视角观察到。

17.10 文档管理视图

文档管理视图对应:

text 复制代码
GET /admin/documents

它支持:

  • userId
  • kbId
  • indexStatus
  • keyword

页面展示:

  • 文档 ID
  • 用户 ID
  • 知识库 ID
  • 文件名
  • 文件类型
  • 索引状态
  • 切片数量
  • 创建时间

这一页和第 6 章到第 8 章关系很密切。

上传文档后,系统不是只保存一个文件名,而是会形成文档元数据:

text 复制代码
document_info

文档的 indexStatus 会随着任务执行发生变化。

所以文档管理视图可以用来回答这些问题:

  • 哪个用户上传了文档?
  • 文档属于哪个知识库?
  • 文档当前是否索引完成?
  • 切片数量是不是合理?
  • 是否存在大量失败文档?

这些问题普通用户端不适合全局展示,但管理员必须能看见。


17.10.5 AdminTasks.vue 完整代码

索引任务管理页面的重点不是复杂前端交互,而是让管理员看到全局任务状态,并能对失败任务发起重试。

下面给出 rag-admin-web/src/views/AdminTasks.vue 的完整代码示例。

vue 复制代码
<template>
  <section class="admin-section">
    <el-form :inline="true" :model="queryForm" class="query-form">
      <el-form-item label="状态">
        <el-select v-model="queryForm.status" clearable placeholder="全部状态" style="width: 160px">
          <el-option label="FAILED" value="FAILED" />
          <el-option label="TIMEOUT" value="TIMEOUT" />
          <el-option label="RUNNING" value="RUNNING" />
          <el-option label="SUCCESS" value="SUCCESS" />
        </el-select>
      </el-form-item>

      <el-form-item label="用户ID">
        <el-input-number v-model="queryForm.userId" :min="1" controls-position="right" />
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="loadTasks">查询</el-button>
      </el-form-item>
    </el-form>

    <el-table :data="tasks" border v-loading="loading">
      <el-table-column prop="taskId" label="任务ID" width="100" />
      <el-table-column prop="documentId" label="文档ID" width="100" />
      <el-table-column prop="kbId" label="知识库ID" width="110" />
      <el-table-column prop="userId" label="用户ID" width="100" />
      <el-table-column prop="status" label="状态" width="120" />
      <el-table-column prop="retryCount" label="重试次数" width="100" />
      <el-table-column prop="errorMessage" label="错误信息" min-width="220" show-overflow-tooltip />
      <el-table-column prop="createdAt" label="创建时间" width="180" />
      <el-table-column label="操作" width="120" fixed="right">
        <template #default="{ row }">
          <el-button
            v-if="canRetry(row.status)"
            type="warning"
            link
            @click="handleRetry(row.taskId)"
          >
            重试
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </section>
</template>

<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getAdminIndexTasks, retryIndexTask, type IndexTask } from '../api/api'

const loading = ref(false)
const tasks = ref<IndexTask[]>([])

const queryForm = reactive<{
  status?: string
  userId?: number
}>({
  status: undefined,
  userId: undefined
})

async function loadTasks() {
  loading.value = true
  try {
    tasks.value = await getAdminIndexTasks({
      status: queryForm.status,
      userId: queryForm.userId
    })
  } finally {
    loading.value = false
  }
}

function canRetry(status: string) {
  // 只允许 FAILED 和 TIMEOUT 重试,避免破坏第 9 章定义的任务状态机。
  return status === 'FAILED' || status === 'TIMEOUT'
}

async function handleRetry(taskId: number) {
  await retryIndexTask(taskId)
  ElMessage.success('重试消息已投递')
  await loadTasks()
}

onMounted(loadTasks)
</script>

这个页面要展示的是"任务为什么失败、能不能恢复"。所以 errorMessageretryCount 和"重试"按钮比页面样式更重要。


17.11 索引任务视图

索引任务视图对应:

text 复制代码
GET /admin/index-tasks
POST /admin/index-tasks/{taskId}/retry

后端控制器是 Task-Service 中的:

text 复制代码
`AdminIndexTaskController`

查询任务

后端支持按这些字段过滤:

  • userId
  • kbId
  • documentId
  • status

当前管理端前端页面主要开放了 status 过滤。

这也是一个可以继续扩展的地方:以后可以把 userId、kbId、documentId 也加入筛选表单。

任务状态

页面会展示任务状态标签。

常见状态包括:

  • WAITING
  • RUNNING
  • SUCCESS
  • FAILED
  • TIMEOUT
  • RETRYING

不同状态对应不同颜色:

  • SUCCESS / INDEXED:成功色。
  • FAILED / TIMEOUT:危险色。
  • RUNNING / INDEXING / RETRYING:警告色。

这和用户端工作台的状态展示保持一致。

失败任务重试

任务视图最重要的操作是重试。

当前页面只允许对:

  • FAILED
  • TIMEOUT

状态的任务点击重试。

这对应前面第 9 章的索引任务可靠性设计。

失败不是终点,而是可恢复状态。

管理员看到任务失败后,可以在后台触发:

text 复制代码
POST /admin/index-tasks/{taskId}/retry

后端再交给 IndexTaskService 处理。

这一步把"排查"和"恢复"连起来了。


17.12 问答日志视图

问答日志视图对应:

text 复制代码
GET /admin/qa-logs

它支持:

  • userId
  • kbId
  • modelFallback

页面展示:

  • 问答日志 ID
  • 用户 ID
  • 知识库 ID
  • 问题
  • 模型名称
  • 模型状态
  • 总耗时
  • 创建时间

这里最关键的是模型状态。

页面会根据返回字段显示:

text 复制代码
modelFallback = true -> 降级
modelSuccess = true -> 成功
否则 -> 失败

这和第 12 章、第 14 章、第 15 章完全对应。

如果用户说"系统没有正常回答",管理员可以打开问答日志,看它到底是:

  • 检索结果为空。
  • 模型调用失败。
  • 模型超时。
  • Sentinel 限流。
  • 触发了 fallback。

当前管理端表格主要展示概览字段,没有展开完整答案和完整错误详情。作为演示台已经够用;如果要做更完整的企业后台,可以增加详情抽屉,把 answermodelErrorMessage、引用片段和 Prompt 上下文展示出来。


17.13 管理端 API 封装

src/api.ts 是管理端前端的请求中心。

它定义了多个 TypeScript 接口:

  • LoginResponse
  • CurrentUser
  • AdminUser
  • AdminKnowledgeBase
  • AdminDocument
  • AdminIndexTask
  • AdminQaLog

这些类型的作用是让前端知道后端会返回哪些字段。

比如 AdminIndexTask 包含:

  • id
  • documentId
  • kbId
  • userId
  • status
  • retryCount
  • maxRetryCount
  • errorMessage
  • workerId
  • startTime
  • endTime

这正好对应任务管理页面需要展示和排查的信息。

请求解包

管理端同样使用统一响应体:

ts 复制代码
export interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

如果 code !== 0,就抛出错误:

ts 复制代码
throw new Error(response.data.message || '请求失败')

这样页面层只需要 try/catch,不用每个接口都重复判断业务状态码。

Token 自动携带

请求拦截器会读取 rag_admin_token,并写入:

text 复制代码
Authorization: Bearer token

这一点和用户端是一致的。

也就是说,用户端和管理端虽然页面不同,但安全访问模式相同:

text 复制代码
前端保存 Token
-> Axios 自动带 Token
-> Gateway 验证 Token
-> Gateway 判断角色
-> Gateway 转发到后端服务

17.13.5 api.ts 完整代码

下面给出 rag-admin-web/src/api/api.ts 的完整示例。所有接口都以 /admin 开头,都会经过 Gateway 的 ADMIN 角色校验。

ts 复制代码
import request from '../utils/request'

export interface User {
  id: number
  username: string
  nickname?: string
  role: 'USER' | 'ADMIN'
  enabled: boolean
  createdAt: string
}

export interface KnowledgeBase {
  id: number
  userId: number
  name: string
  description?: string
  status: string
  createdAt: string
}

export interface Document {
  id: number
  kbId: number
  userId: number
  fileName: string
  fileType: string
  indexStatus: string
  chunkCount: number
  errorMessage?: string
  createdAt: string
}

export interface IndexTask {
  taskId: number
  documentId: number
  kbId: number
  userId: number
  status: string
  retryCount: number
  maxRetryCount: number
  errorMessage?: string
  createdAt: string
}

export interface QaLog {
  id: number
  kbId: number
  userId: number
  question: string
  answer: string
  modelName: string
  costTimeMs: number
  modelSuccess: boolean
  modelFallback: boolean
  createdAt: string
}

// 用户管理:由 Gateway 转发到 auth-service。
export function getUsers(params?: { username?: string; role?: string }) {
  return request.get<User[]>('/admin/users', { params })
}

export function updateUserRole(userId: number, role: string) {
  return request.put<void>(`/admin/users/${userId}/role`, null, {
    params: { role }
  })
}

export function updateUserStatus(userId: number, enabled: boolean) {
  return request.put<void>(`/admin/users/${userId}/status`, null, {
    params: { enabled }
  })
}

// 知识库与文档:由 Gateway 转发到 knowledge-service。
export function getAllKnowledgeBases(params?: { userId?: number; keyword?: string }) {
  return request.get<KnowledgeBase[]>('/admin/knowledge-bases', { params })
}

export function getAllDocuments(params?: { userId?: number; kbId?: number }) {
  return request.get<Document[]>('/admin/documents', { params })
}

// 索引任务:由 Gateway 转发到 task-service。
export function getAdminIndexTasks(params?: { status?: string; userId?: number }) {
  return request.get<IndexTask[]>('/admin/index-tasks', { params })
}

export function retryIndexTask(taskId: number) {
  return request.post<void>(`/admin/index-tasks/${taskId}/retry`)
}

// 问答日志:管理端按全局维度查询,用于观察模型调用、耗时和降级情况。
export function getAdminQaLogs(params?: { userId?: number; kbId?: number; modelFallback?: boolean }) {
  return request.get<QaLog[]>('/admin/qa-logs', { params })
}

这些方法和用户端 API 的区别在于:用户端接口强调"当前用户自己的数据",管理端接口强调"全局数据 + 条件筛选"。权限边界不靠这些 TypeScript 方法保证,而是靠 Gateway 的 /admin/** ADMIN 校验保证。


17.14 管理端如何支持项目演示

这一章标题里还有"演示支持"。

这是因为对于学生项目、毕业设计、实习展示和面试作品来说,光有功能还不够,还要能讲清楚、演示清楚。

KnowHub 的演示可以按下面的顺序准备。

第一步:准备两个普通用户和一个管理员

先注册几个账号:

text 复制代码
user_a
user_b
admin

然后把 admin 升级为管理员:

sql 复制代码
UPDATE user_account SET role = 'ADMIN' WHERE username = 'admin';

重新登录管理端后,JWT 中就会携带 role=ADMIN

第二步:普通用户创建知识库

用 user_a 登录用户端,创建一个知识库。

再进入管理端的"知识库"视图,按 userId 或 keyword 查询。

这样可以展示:

text 复制代码
用户端产生业务数据,管理端能从全局视角观察。

第三步:上传文档并观察索引

在用户端上传文档,然后切到管理端的"文档"和"任务"视图。

你可以展示:

  • 文档记录出现。
  • indexStatus 变化。
  • 索引任务出现。
  • 成功后任务变成 SUCCESS。
  • 失败后任务出现 errorMessage。

这一步能把第 7 章到第 11 章的内容串起来。

第四步:发起 RAG 问答

在用户端问一个和文档相关的问题。

然后在管理端打开"问答日志"。

你可以展示:

  • 问题记录。
  • 使用的模型。
  • 模型是否成功。
  • 是否发生降级。
  • 总耗时。

这一步能把第 12 章到第 15 章串起来。

第五步:演示异常和恢复

如果你想展示项目更有工程含量,可以准备一次失败演示。

比如:

  • 停掉模型服务,让问答触发 fallback。
  • 制造索引失败任务,再在管理端点击重试。
  • 调低 Sentinel 阈值,演示限流和降级。

演示时不要只说"我做了 AI 问答"。

更好的表达是:

text 复制代码
这个系统不仅能回答问题,还能观察文档索引状态、任务失败原因、模型调用状态和降级日志。

这会比单纯展示一个问答框更有说服力。


17.14.7 完整演示操作步骤

下面把前面五步演示目标展开成可以直接照着做的操作步骤。

步骤一:准备管理员账号(详见 17.3 基础 RBAC)

用 SQL 工具连接 MySQL,执行:

sql 复制代码
UPDATE user_account SET role = 'ADMIN' WHERE username = 'admin';

确认 admin 账号的 role 字段已经变为 ADMIN。然后重新登录管理端,让新签发的 JWT 携带 role=ADMIN

步骤二:启动管理端前端(详见 17.7.1 如何启动管理端前端)

进入 rag-admin-web 目录:

bash 复制代码
npm run dev

在浏览器打开:

text 复制代码
http://localhost:5174

看到登录页后,输入 admin 账号和密码登录。预期跳转到管理端主页,并看到"用户管理、知识库、文档、任务、日志"五个 Tab。

步骤三:查看全局用户(详见 17.8 用户管理视图)

切换到"用户管理" Tab,点击查询。

预期结果:

  • 能看到全部用户列表,包括 user_auser_badmin
  • adminrole 列显示为 ADMIN
  • 普通用户的 role 列显示为 USER

这一步展示的是管理端和用户端的核心差异:管理端查的是全局用户数据。

步骤四:用户端创建数据,管理端观察全局数据(详见第 16 章用户端工作台、17.9 和 17.10)

user_a 登录用户端:

text 复制代码
http://localhost:5173

创建一个知识库,并上传一个测试文档。

然后切换回管理端,在"知识库"和"文档" Tab 中按 userId 查询。

预期结果:

  • 能看到 user_a 创建的知识库。
  • 能看到该知识库下的文档。
  • 文档的 indexStatus 会从 INDEXING 变为 INDEXEDFAILED

步骤五:发起问答并查看 qa_log(详见第 12 章 RAG 问答闭环、17.12 问答日志视图)

在用户端发起一次和测试文档相关的问答。

切换到管理端的"问答日志" Tab,按 userId 查询。

预期结果:

  • 能看到刚才的问答记录。
  • 记录中包含问题、答案、模型名称、总耗时。
  • modelSuccessmodelFallback 能反映本次模型调用状态。

步骤六:异常演示与恢复(详见第 9 章任务重试、第 15 章异常演练)

临时停止 Embedding 模型服务,然后在用户端上传一个新文档。

等待约 30 秒后,在管理端"任务" Tab 查询:

text 复制代码
status=FAILED

预期结果:

  • 新任务处于 FAILED 状态。
  • errorMessage 包含模型连接失败或认证失败信息。

恢复 Embedding 模型服务后,点击"重试"按钮。

再次刷新任务列表,预期任务最终变为 SUCCESS

演示前建议准备好 Postman 或浏览器开发者工具。必要时同时展示 Gateway 的请求转发日志和后端服务的业务日志,这样能把"前端操作、网关鉴权、服务处理、任务状态变化"完整串起来。


17.15 当前管理端的边界

本章要真实,不要把当前管理端夸大成完整商业后台。

当前 rag-admin-web 是一个基础管理控制台,它已经能支撑:

  • 管理员登录。
  • 用户列表查询。
  • 用户角色调整。
  • 用户启用和禁用。
  • 全局知识库查询。
  • 全局文档查询。
  • 全局索引任务查询。
  • 失败和超时任务重试。
  • 全局问答日志查询。

但它还不是完整企业级后台。

后续可以扩展:

  • 分页组件,而不是只靠 LIMIT。
  • 操作审计,记录管理员改了什么。
  • 详情抽屉,展示文档错误、问答答案、模型错误详情。
  • 图表大盘,展示任务成功率、失败率、平均耗时。
  • 更细粒度 RBAC,比如菜单权限和按钮权限。
  • 批量处理,比如批量重试失败任务。
  • 管理端路由拆分,让每个模块独立维护。

这种写法比较实事求是。

真实项目简历和答辩中,最怕把"基础后台"说成"大型权限系统"。那样很容易被追问穿。

更稳的表达是:

text 复制代码
我实现了基础 USER/ADMIN RBAC 和管理端全局观察能力,并预留了细粒度权限、审计和统计大盘的扩展方向。

这样既真实,也体现工程理解。


17.16 本章和前面章节的关系

管理端不是新开一条线,它是前面所有能力的总览入口。

可以这样对应:

text 复制代码
第 4 章:JWT 登录认证 -> 管理员登录和 role claim
第 5 章:用户资源隔离 -> 普通用户看自己,管理员看全局
第 6 章:知识库与文档元数据 -> 后台知识库和文档管理
第 9 章:RabbitMQ 与索引任务 -> 后台任务查询和重试
第 12 章:RAG 问答闭环 -> 后台问答日志
第 14 章:Sentinel 与 AI 降级 -> modelFallback 观察
第 15 章:异常演练与排障 -> 管理端辅助定位问题
第 16 章:用户端前端 -> 普通用户工作台
第 17 章:后台管理端 -> 管理员观察台和演示支持

有了用户端,系统能被普通用户使用。

有了管理端,系统能被维护、观察和讲清楚。

这就是本章的价值。


本章小结

这一章我们讲了后台管理端与演示支持。

KnowHub 的后台管理端由 rag-admin-web 和后端 /admin/** 接口组成。前端当前是一个 Vue 3 + Element Plus 单页控制台,核心逻辑集中在 App.vueapi.ts;后端管理接口分布在 auth-service、knowledge-service 和 task-service;Gateway 负责把 /admin/users/**/admin/knowledge-bases/**/admin/documents/**/admin/qa-logs/**/admin/index-tasks/** 转发到对应服务,并通过 JWT 中的 role=ADMIN 进行访问控制。

本章最重要的点是:管理端不是用户端的重复,而是平台全局观察和问题处理入口。它可以管理用户角色和状态,查看全局知识库、文档、索引任务和问答日志,还可以对失败或超时任务发起重试。对于项目演示来说,管理端能把用户操作背后的数据变化、任务状态和模型调用状态展示出来,让系统从"能问答"提升到"能解释、能排查、能展示工程链路"。

下一章,我们会进入接口回归、异常演练与轻量压测,验证前面已经完成的后端、用户端和管理端是否能稳定串起来。

本章涉及的关键类与文件

text 复制代码
rag-gateway-service/src/main/resources/
  application.yml                         (后台路由配置)

rag-gateway-service/src/main/java/.../filter/
  JwtAuthGlobalFilter.java                (管理端权限校验)

auth-service/src/main/java/.../admin/
  AdminUserController.java                (用户管理接口)

knowledge-service/src/main/java/.../admin/
  AdminKnowledgeBaseController.java       (知识库管理接口)
  AdminDocumentController.java            (文档管理接口)

task-service/src/main/java/.../admin/
  AdminIndexTaskController.java           (任务管理接口)
  AdminQaLogController.java               (日志管理接口)

rag-admin-web/src/
  App.vue                                 (管理端主页,五个 Tab)
  api/api.ts                              (管理端接口调用封装)
  utils/request.ts                        (Axios 封装,同用户端)
  utils/token.ts                          (Token 管理,同用户端)
  views/                                  (如果拆分页面,放在此目录)
    AdminTasks.vue                        (任务管理页面示例)

这张清单帮助读者在动手前建立整体感知:管理端不是一个孤立前端页面,而是 Gateway 权限校验、后端全局查询接口和前端观察台共同组成的一条演示支撑链路。


思考题

  1. 为什么后台管理端不能只看作用户端的另一个页面?
  2. USER 和 ADMIN 这两个角色分别解决什么问题?
  3. 为什么管理员账号当前采用"先注册,再更新 role"的方式更适合教学演示?
  4. JWT 中为什么要写入 role 字段?
  5. 为什么 /admin/** 的权限校验必须放在 Gateway,而不能只放在前端?
  6. Gateway 为什么要覆盖外部传入的 X-User-Role 请求头?
  7. /admin/users/**/admin/knowledge-bases/**/admin/index-tasks/** 为什么会被路由到不同服务?
  8. 用户管理视图中,为什么角色和状态更新仍然要在后端校验?
  9. 文档管理视图为什么要展示 indexStatuschunkCount
  10. 索引任务视图为什么只允许 FAILED 和 TIMEOUT 任务重试?
  11. 问答日志中的 modelFallback 对排查模型问题有什么帮助?
  12. 如果你要把当前管理端升级成更完整的企业后台,你会优先补哪些能力?

参考答案

1. 为什么后台管理端不能只看作用户端的另一个页面?

因为两者的目标完全不同。用户端是个人工作台,面向普通用户,只展示"我的知识库、我的文档、我的问答、我的任务状态",解决的是"我的业务能不能完成";管理端是平台观察台,面向管理员,展示"全部用户、全部知识库、全部文档、全部索引任务、全部问答日志",解决的是"整个平台是否健康"。管理端还要承担排查、治理和演示支撑的职责,这些都不是用户端页面的简单扩展。

2. USER 和 ADMIN 这两个角色分别解决什么问题?

  • USER(普通用户):解决"普通用户不能访问后台接口"的问题,保证普通用户只能操作自己的资源。
  • ADMIN(管理员):解决"管理员可以查看全局数据"的问题,让管理员能观察整个平台的运行状态并对关键对象(用户角色、任务重试等)进行处理。

3. 为什么管理员账号当前采用"先注册,再更新 role"的方式更适合教学演示?

因为这种方式简单、可控、不会把管理员密码写进 SQL。先正常注册账号,再手动执行 UPDATE user_account SET role = 'ADMIN' WHERE username = '...' 把账号升级为管理员,避免了在 SQL 脚本里硬编码管理员密码的安全隐患,也省去了复杂的开通流程,非常适合学习和演示场景。

4. JWT 中为什么要写入 role 字段?

因为 Gateway 不会每次都去查数据库,它主要看 Token。登录成功签发 JWT 时把角色写进 Token(ROLE_CLAIM = "role"),Gateway 解析 Token 后就能直接知道当前请求者是 USER 还是 ADMIN,从而快速判断是否允许访问 /admin/** 接口,无需每次请求都回源数据库查询角色。

5. 为什么 /admin/** 的权限校验必须放在 Gateway,而不能只放在前端?

因为前端判断只是改善体验,不是安全边界。用户只要绕过前端页面,直接构造请求调用 /admin/** 接口,就可能访问管理员数据。只有把权限校验放在 Gateway(和后端二次防御),才能保证无论请求来自哪里,只要不是 ADMIN 角色就会被统一拦截。KnowHub 的做法是"前端检查 ADMIN -> Gateway 再检查 ADMIN -> 后端只暴露 /admin/** 管理接口"三层防线。

6. Gateway 为什么要覆盖外部传入的 X-User-Role 请求头?

为了防止身份伪造。攻击者可以自己构造 X-User-Id: 1X-User-Role: ADMIN 这样的请求头来冒充管理员。Gateway 不信任外部传入的用户头,而是先删掉它们,再根据 JWT 解析出的可信身份重新写入用户信息,确保下游服务拿到的身份一定是经过认证的。

7. /admin/users/**/admin/knowledge-bases/**/admin/index-tasks/** 为什么会被路由到不同服务?

因为这三类数据分别属于不同的微服务:用户账号、角色、状态属于认证服务,所以 /admin/users/** 转发到 rag-auth-service;知识库、文档和问答日志是知识库服务的数据,所以 /admin/knowledge-bases/**/admin/documents/**/admin/qa-logs/** 转发到 rag-knowledge-service;索引任务的执行和重试属于任务服务,所以 /admin/index-tasks/** 转发到 rag-task-service。这与前面微服务拆分的职责划分保持一致。

8. 用户管理视图中,为什么角色和状态更新仍然要在后端校验?

因为后端不能依赖前端下拉框传来的字符串。前端可能被绕过或篡改,后端必须自己校验:角色必须是 USERADMIN(如 normalizeStrictRole 校验),状态值必须合法(如 @Min(0)@Max(1))。后台管理端修改的是平台级数据,不能随意接受非法状态值,所以后端校验必不可少。

9. 文档管理视图为什么要展示 indexStatuschunkCount

因为这两个字段能反映文档索引的健康状况。indexStatus 展示文档当前是否索引完成(INDEXINGINDEXEDFAILED),chunkCount 展示切片数量是否合理。管理员据此可以回答:哪个用户上传了文档、文档属于哪个知识库、是否索引完成、切片数量是否合理、是否存在大量失败文档等问题,这是普通用户端不适合全局展示、但管理员必须能看见的信息。

10. 索引任务视图为什么只允许 FAILED 和 TIMEOUT 任务重试?

因为这对应第 9 章的索引任务可靠性设计。只有 FAILEDTIMEOUT 状态的任务是"可恢复"的失败状态,允许重试;而 RUNNING 表示任务还在执行中,SUCCESS 表示已完成,都不应该重试。这样能避免破坏任务状态机,保证重试逻辑的正确性。

11. 问答日志中的 modelFallback 对排查模型问题有什么帮助?

modelFallback 能直接告诉管理员本次问答是否触发了 AI 降级。当用户反馈"系统没有正常回答"时,管理员打开问答日志,结合 modelFallbackmodelSuccess 等字段就能快速判断问题属于哪一类:检索结果为空、模型调用失败、模型超时、Sentinel 限流,还是触发了 fallback。这和第 12、14、15 章的内容完全对应,是定位模型链路问题的关键线索。

12. 如果你要把当前管理端升级成更完整的企业后台,你会优先补哪些能力?

我会优先补齐以下能力(按优先级):

  1. 分页组件 :替代只靠 LIMIT 100 的简单保护,支持大数据量下的稳定查询。
  2. 操作审计:记录管理员改了什么(角色调整、状态变更、任务重试),满足合规和追溯需求。
  3. 详情抽屉:展示文档错误详情、问答完整答案、模型错误信息、引用片段和 Prompt 上下文,提升排查能力。
  4. 图表大盘:展示任务成功率、失败率、平均耗时等统计指标,让管理员一眼看到平台健康度。
  5. 更细粒度 RBAC:从 USER/ADMIN 两级扩展到菜单权限、按钮权限。
  6. 批量处理:如批量重试失败任务,提升运维效率。
  7. 管理端路由拆分:把单页多 Tab 拆成独立路由页面和 store,便于后续扩展维护。
相关推荐
万少3 小时前
DeepSeek-V4-Flash 正式版上线了,但这 3 个坑我帮你提前踩了
前端·javascript·后端
宁风NF3 小时前
JavaScript:网络请求与前端通信
开发语言·前端·javascript·网络·学习·ecmascript
猫猫不是喵喵.3 小时前
Vue3 中 computed 计算属性与 watch、watchEffect 监听
前端·javascript·vue.js
En^_^Joy4 小时前
Vue项目创建与入口配置全攻略
前端·vue.js·arcgis
猫猫不是喵喵.4 小时前
Vue2 的 Vuex 状态管理与 Vue3 的 Pinia 状态管理
前端·javascript·vue.js
看昭奚恤哭5 小时前
Flutter 布局核心思想
开发语言·javascript·flutter
朝阳5816 小时前
Cesium `Camera.flyTo`:“飞过去“
javascript
宁风NF6 小时前
JavaScript:内存、垃圾回收、性能优化
开发语言·前端·javascript·学习·性能优化·es6
午安~婉7 小时前
挑战二:博客预览卡片
前端·javascript·html