Files
郑龙捷 74dd97428f feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。

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

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

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

- 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
2026-09-02 15:02:47 +08:00

214 lines
7.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const MODEL_CATEGORIES = [
{ id: "std", name: "标准A卡" },
{ id: "bai", name: "白户A卡" },
{ id: "big", name: "大额A卡" },
{ id: "afd", name: "反欺诈评分" },
] as const;
export type ModelCategoryId = (typeof MODEL_CATEGORIES)[number]["id"];
export type ModelGrade = "A" | "B" | "C";
export type AbnormalLevel = "三级" | "二级" | "一级" | "正常";
export type ModelStatus = "正常" | "陪跑" | "陪跑结束" | "下线";
export type RankingStatus = "相符" | "不符" | "—";
export type ModelRecord = {
bank: string;
category: ModelCategoryId;
name: string;
modelId: string;
version: string;
status: ModelStatus;
iteratedAt: string;
ranking: RankingStatus;
ks: number;
psi: number;
ksDrop: number;
secondaryHits: number;
wuji: boolean;
processedAt: string | null;
previousAdvice: string;
commonModel: string | null;
};
export type MonitoringRow = ModelRecord & {
monitorMonth: string;
};
export const MONITOR_MONTHS = [
"2026-02",
"2026-03",
"2026-04",
"2026-05",
"2026-06",
"2026-07",
] as const;
// 业务数据全部来自后端。以下空集合仅保留页面类型和计算函数的接口,
// 在真实接口补齐对应字段前不再提供本地样例数据。
export const MODELS: ModelRecord[] = [];
export const FEATURE_METRICS: Array<{
key: string;
name: string;
iv: number;
ivDrop: number;
csi: number;
csiRise: number;
ksContribution: number;
psiContribution: number;
}> = [];
export const SORTING_DISTRIBUTION = {
bins: [] as string[],
counts: [] as number[],
badRates: [] as number[],
};
export const BANK_AVERAGE_CYCLE: Record<string, number> = {};
export type ModelLifecycle = {
onlineAt: string | null;
escortStartAt: string | null;
escortEndAt: string | null;
offlineAt: string | null;
developer: string;
developmentKs: number;
developmentPsi: number;
maxLift: number;
};
export const MODEL_LIFECYCLE: Record<string, ModelLifecycle> = {};
export function categoryName(category: ModelCategoryId): string {
return MODEL_CATEGORIES.find((item) => item.id === category)?.name ?? category;
}
export function abnormalLevelOf(model: Pick<ModelRecord, "ranking" | "ks" | "psi" | "ksDrop">): AbnormalLevel {
const { ranking, ks, psi, ksDrop } = model;
if (ranking === "—") return "正常";
if (ranking === "不符" && ks < 40 && psi > 10) return "三级";
if (ranking === "相符" && ks < 40 && psi > 25) return "三级";
if (ranking === "不符" && ks >= 40 && psi > 25) return "二级";
if (ranking === "不符" && ks >= 40 && psi > 10 && psi <= 25 && ksDrop > 20) return "二级";
if (ranking === "不符" && ks < 40 && psi <= 10) return "二级";
if (ranking === "相符" && ks < 40 && psi <= 25) return "二级";
if (ranking === "相符" && ks >= 40 && psi > 25 && ksDrop > 20) return "二级";
if (ranking === "不符" && ks >= 40 && psi > 10 && psi <= 25 && ksDrop <= 20) return "一级";
if (ranking === "不符" && ks >= 40 && psi <= 10) return "一级";
if (ranking === "相符" && ks >= 40 && psi > 25 && ksDrop <= 20) return "一级";
if (ranking === "相符" && ks >= 40 && psi > 10 && psi <= 25) return "一级";
if (ranking === "相符" && ks >= 40 && psi <= 10 && ksDrop > 20) return "一级";
return "正常";
}
export function gradeOf(model: Pick<ModelRecord, "ranking" | "ks" | "psi" | "ksDrop" | "secondaryHits" | "status">): ModelGrade {
if (model.status === "下线") return "A";
const abnormal = abnormalLevelOf(model);
if (abnormal === "三级") return "C";
if (abnormal === "二级" && model.secondaryHits >= 4) return "C";
if (abnormal === "正常") return "A";
return "B";
}
export function abnormalReasonOf(model: ModelRecord | MonitoringRow): string {
const abnormal = abnormalLevelOf(model);
if (abnormal === "正常") return "各项指标均在阈值内";
const reasons: string[] = [];
if (model.ranking === "不符") reasons.push("排序性不符");
if (model.ks < 40) reasons.push("KS 低于 40%");
if (model.psi > 25) reasons.push("PSI 高于 25%");
else if (model.psi > 10) reasons.push("PSI 处于 10%25%");
if (model.ksDrop > 20) reasons.push("KS 环比降幅高于 20%");
if (abnormal === "二级") {
const result = model.secondaryHits >= 4 ? ",已触发监控结果升级为 C" : ",未达到累计升级条件";
return `${reasons.join("、")};近 6 个月累计 ${model.secondaryHits} 次二级异常${result}`;
}
return reasons.join("、");
}
export function modelTrend(
_model: ModelRecord,
_metric: "ks" | "psi",
_months: readonly string[] = MONITOR_MONTHS,
): number[] {
return [];
}
export function monitoringRows(_source: ModelRecord[] = MODELS): MonitoringRow[] {
return [];
}
export function monthsBetween(from: string, to: string): string[] {
const [fromYear, fromMonth] = from.split("-").map(Number);
const [toYear, toMonth] = to.split("-").map(Number);
if (!fromYear || !fromMonth || !toYear || !toMonth) return [...MONITOR_MONTHS];
const start = fromYear * 12 + fromMonth - 1;
const end = toYear * 12 + toMonth - 1;
if (start > end || end - start > 23) return [...MONITOR_MONTHS];
return Array.from({ length: end - start + 1 }, (_, index) => {
const value = start + index;
const year = Math.floor(value / 12);
const month = value % 12 + 1;
return `${year}-${String(month).padStart(2, "0")}`;
});
}
export function average(values: number[]): number {
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
}
export function categoryTrend(
_category: ModelCategoryId,
_metric: "ks" | "psi",
_months: string[],
_source: ModelRecord[] = MODELS,
): number[] {
return [];
}
export function bankTrend(
_bank: string,
_metric: "ks" | "psi",
_months: string[],
_source: ModelRecord[] = MODELS,
): number[] {
return [];
}
export function modelsTrend(
_models: ModelRecord[],
_metric: "ks" | "psi",
_months: string[],
): number[] {
return [];
}
export function latestIterationDate(models: ModelRecord[]): string {
return models.map((model) => model.iteratedAt).filter(Boolean).sort().at(-1) ?? "—";
}
export function modelIterationCycleMonths(model: ModelRecord): number | null {
const lifecycle = MODEL_LIFECYCLE[model.modelId];
const start = lifecycle?.onlineAt ?? lifecycle?.escortStartAt;
if (!start || !model.iteratedAt) return null;
const [startYear, startMonth] = start.slice(0, 7).split("-").map(Number);
const [endYear, endMonth] = model.iteratedAt.slice(0, 7).split("-").map(Number);
if (![startYear, startMonth, endYear, endMonth].every(Number.isFinite)) return null;
return Math.max(0, (endYear * 12 + endMonth) - (startYear * 12 + startMonth));
}
export function averageIterationCycle(models: ModelRecord[]): number | null {
const values = models.map(modelIterationCycleMonths).filter((value): value is number => value !== null);
return values.length ? Number(average(values).toFixed(1)) : null;
}
export function averageIterationMonths(models: ModelRecord[]): number {
const months = models
.filter((model) => model.status !== "下线")
.map((model) => {
const [year, month] = model.iteratedAt.slice(0, 7).split("-").map(Number);
return (2026 * 12 + 7) - (year * 12 + month);
})
.filter(Number.isFinite);
return Number(average(months).toFixed(1));
}