feat: 接入运维真实数据与三角色权限链路

- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。

- 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。

- 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。

- 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。

- 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
郑龙捷
2026-09-02 15:02:47 +08:00
parent 6d044784ec
commit 74dd97428f
51 changed files with 2142 additions and 1062 deletions
+1 -66
View File
@@ -1,16 +1,3 @@
export type DemoUser = {
userId: string;
userName: string;
username: string;
roleCode: "admin" | "developer";
roleName: string;
};
export type DemoWorkspace = {
workspaceId: string;
workspaceName: string;
};
export function createUuid(): string {
const cryptoApi = globalThis.crypto;
if (typeof cryptoApi?.randomUUID === "function") {
@@ -40,58 +27,6 @@ export function createUuid(): string {
].join("-");
}
export const demoUsers: DemoUser[] = [
{ userId: "0000000000RF6FG1SDBXG59S13", userName: "张三", username: "admin-zhang", roleCode: "admin", roleName: "管理员" },
{ userId: "0000000000H2QYCGPCWQM1JSGS", userName: "李四", username: "admin-li", roleCode: "admin", roleName: "管理员" },
{ userId: "0000000000RWG40ESZPGJT629J", userName: "王五", username: "dev-wang", roleCode: "developer", roleName: "开发人员" },
{ userId: "00000000004CQV7WASJA6N6FW4", userName: "赵六", username: "dev-zhao", roleCode: "developer", roleName: "开发人员" },
];
export const demoWorkspaces: DemoWorkspace[] = [
{ workspaceId: "00000000000BM630VT9ARVFZPC", workspaceName: "模型开发 Workspace" },
{ workspaceId: "0000000000AE0NC0V5T424KK86", workspaceName: "风险验证 Workspace" },
];
function readStoredContext(): Partial<{
userId: string;
workspaceId: string;
}> {
if (typeof window === "undefined") return {};
try {
return JSON.parse(
window.localStorage.getItem("model-platform-demo-context") ?? "{}",
) as Partial<{ userId: string; workspaceId: string }>;
} catch {
return {};
}
}
const storedContext = readStoredContext();
const initialUser = demoUsers.find((item) => item.userId === storedContext.userId)
?? demoUsers[0];
const initialWorkspace = demoWorkspaces.find(
(item) => item.workspaceId === storedContext.workspaceId,
) ?? demoWorkspaces[0];
export const demoContext = {
...initialUser,
...initialWorkspace,
};
export function setDemoContext(input: {
user?: DemoUser;
workspace?: DemoWorkspace;
}): void {
if (input.user) Object.assign(demoContext, input.user);
if (input.workspace) Object.assign(demoContext, input.workspace);
if (typeof window !== "undefined") {
window.localStorage.setItem("model-platform-demo-context", JSON.stringify({
userId: demoContext.userId,
workspaceId: demoContext.workspaceId,
}));
}
}
// API client for the platform backend.
//
// All endpoints that take a workspace context require the caller to
@@ -111,7 +46,7 @@ export type Employee = {
display_name: string;
email: string | null;
status: "active" | "disabled" | "locked";
role_code: "admin" | "developer";
role_code: string;
role_name: string;
created_at: string;
};
+2 -2
View File
@@ -245,7 +245,7 @@ export type WorkspaceBoundApi = {
display_name: string;
email?: string | undefined;
password: string;
role_code?: "admin" | "developer";
role_code?: string;
},
) => Promise<Employee>;
updateEmployee: (
@@ -257,7 +257,7 @@ export type WorkspaceBoundApi = {
input: {
display_name?: string;
email?: string | null;
role_code?: "admin" | "developer";
role_code?: string;
status?: "active" | "disabled" | "locked";
},
) => Promise<Employee>;
@@ -25,7 +25,7 @@ export async function createPlatformEmployee(
display_name: string;
email?: string | undefined;
password: string;
role_code?: "admin" | "developer";
role_code?: string;
},
): Promise<Employee> {
return apiRequest<Employee>(
@@ -39,7 +39,7 @@ export async function updatePlatformEmployee(
input: {
display_name?: string;
email?: string | null;
role_code?: "admin" | "developer";
role_code?: string;
status?: "active" | "disabled" | "locked";
},
): Promise<Employee> {
+51 -44
View File
@@ -1,14 +1,12 @@
import {
MODELS,
monitoringRows,
type ModelCategoryId,
type ModelRecord,
type ModelStatus,
type MonitoringRow,
type RankingStatus,
import type {
ModelCategoryId,
ModelRecord,
ModelStatus,
MonitoringRow,
RankingStatus,
} from "~/features/operations/modelData";
export type OperationsApiMode = "mock" | "api";
export type OperationsApiMode = "api";
export type OperationsModelListParams = {
workspaceId?: string;
@@ -49,6 +47,41 @@ export type MonthlyMonitoringResultDto = {
secondary_hits_6m: number;
};
export type WorkbenchTone = "normal" | "warning" | "danger" | "primary";
export type OperationsWorkbenchKpi = {
key: string;
label: string;
value: number;
detail: string;
target: string;
tone: WorkbenchTone;
};
export type OperationsWorkbenchItem = {
id: string;
tone: WorkbenchTone;
title: string;
detail: string;
action_label: string;
target: string;
};
export type OperationsWorkbenchActivity = {
occurred_at: string;
actor: string;
text: string;
};
export type OperationsWorkbenchDto = {
role: "admin" | "model_team" | "business_team";
alerts: string[];
kpis: OperationsWorkbenchKpi[];
todos: OperationsWorkbenchItem[];
watches: OperationsWorkbenchItem[];
activities: OperationsWorkbenchActivity[];
};
type ApiEnvelope<T> = {
data: T;
meta?: Record<string, unknown>;
@@ -66,8 +99,7 @@ export class OperationsApiError extends Error {
}
}
const configuredMode = import.meta.env.VITE_OPERATIONS_API_MODE;
export const operationsApiMode: OperationsApiMode = configuredMode === "api" ? "api" : "mock";
export const operationsApiMode: OperationsApiMode = "api";
const API_BASE = "/api/v1/operations";
function requireWorkspaceId(workspaceId?: string): string {
@@ -117,28 +149,7 @@ function toModelRecord(dto: OperationsModelDto): ModelRecord {
};
}
function mockDelay(signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(resolve, 160);
signal?.addEventListener("abort", () => {
window.clearTimeout(timer);
reject(new DOMException("Request aborted", "AbortError"));
}, { once: true });
});
}
export async function listOperationsModels(params: OperationsModelListParams = {}): Promise<ModelRecord[]> {
if (operationsApiMode === "mock") {
await mockDelay(params.signal);
const keyword = params.keyword?.trim().toLowerCase();
return MODELS
.filter((model) => !params.bank || model.bank === params.bank)
.filter((model) => !params.category || model.category === params.category)
.filter((model) => !params.status || model.status === params.status)
.filter((model) => !keyword || `${model.bank} ${model.name} ${model.modelId} ${model.version}`.toLowerCase().includes(keyword))
.map((model) => ({ ...model }));
}
const query = new URLSearchParams();
query.set("workspace_id", requireWorkspaceId(params.workspaceId));
if (params.bank) query.set("bank", params.bank);
@@ -155,12 +166,6 @@ export async function getOperationsModel(
workspaceId?: string,
signal?: AbortSignal,
): Promise<ModelRecord> {
if (operationsApiMode === "mock") {
await mockDelay(signal);
const model = MODELS.find((item) => item.modelId === modelId);
if (!model) throw new OperationsApiError("模型不存在", 404, "MODEL_NOT_FOUND");
return { ...model };
}
const query = new URLSearchParams({ workspace_id: requireWorkspaceId(workspaceId) });
const data = await request<OperationsModelDto>(`/models/${encodeURIComponent(modelId)}?${query.toString()}`, { signal });
return toModelRecord(data);
@@ -172,12 +177,6 @@ export async function getMonthlyMonitoringResult(
workspaceId?: string,
signal?: AbortSignal,
): Promise<MonitoringRow> {
if (operationsApiMode === "mock") {
await mockDelay(signal);
const result = monitoringRows(MODELS).find((item) => item.modelId === modelId && item.monitorMonth === month);
if (!result) throw new OperationsApiError("该月份暂无监控结果", 404, "MONITOR_RESULT_NOT_FOUND");
return { ...result };
}
const model = await getOperationsModel(modelId, workspaceId, signal);
const query = new URLSearchParams({
month,
@@ -197,3 +196,11 @@ export async function getMonthlyMonitoringResult(
secondaryHits: data.secondary_hits_6m,
};
}
export async function getOperationsWorkbench(
workspaceId?: string,
signal?: AbortSignal,
): Promise<OperationsWorkbenchDto> {
const query = new URLSearchParams({ workspace_id: requireWorkspaceId(workspaceId) });
return request<OperationsWorkbenchDto>(`/workbench?${query.toString()}`, { signal });
}