- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
214 lines
7.2 KiB
TypeScript
214 lines
7.2 KiB
TypeScript
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));
|
||
}
|