feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -43,134 +43,25 @@ export const MONITOR_MONTHS = [
|
||||
"2026-07",
|
||||
] as const;
|
||||
|
||||
export const MODELS: ModelRecord[] = [
|
||||
{
|
||||
bank: "江城银行", category: "std", name: "标准A卡", modelId: "JC-STD-001", version: "v2.3",
|
||||
status: "正常", iteratedAt: "2026-06-18", ranking: "不符", ks: 36.84, psi: 27.4,
|
||||
ksDrop: 24.1, secondaryHits: 3, wuji: true, processedAt: "2026-06-18",
|
||||
previousAdvice: "建议启动模型微调或重构评估", commonModel: "标准A卡通用版 v2.3",
|
||||
},
|
||||
{
|
||||
bank: "滨海银行", category: "std", name: "标准A卡", modelId: "BH-STD-001", version: "v2.3",
|
||||
status: "陪跑", iteratedAt: "2026-07-28", ranking: "相符", ks: 44.1, psi: 6.2,
|
||||
ksDrop: 3.4, secondaryHits: 0, wuji: false, processedAt: "2026-06-03",
|
||||
previousAdvice: "维持现状,继续监控", commonModel: "标准A卡通用版 v2.3",
|
||||
},
|
||||
{
|
||||
bank: "华东银行", category: "std", name: "标准A卡", modelId: "HD-STD-001", version: "v2.2",
|
||||
status: "正常", iteratedAt: "2026-03-20", ranking: "相符", ks: 43.5, psi: 8.8,
|
||||
ksDrop: 6.1, secondaryHits: 0, wuji: false, processedAt: "2026-05-20",
|
||||
previousAdvice: "维持现状,继续监控", commonModel: "标准A卡通用版 v2.3",
|
||||
},
|
||||
{
|
||||
bank: "南岭银行", category: "std", name: "标准A卡", modelId: "NL-STD-001", version: "v1.6",
|
||||
status: "正常", iteratedAt: "2025-08-22", ranking: "相符", ks: 41.12, psi: 13.6,
|
||||
ksDrop: 22.5, secondaryHits: 1, wuji: false, processedAt: "2026-06-12",
|
||||
previousAdvice: "建议重点关注 PSI 与 KS 环比变化", commonModel: "标准A卡通用版 v2.3",
|
||||
},
|
||||
{
|
||||
bank: "云岭银行", category: "std", name: "标准A卡", modelId: "YL-STD-001", version: "v2.1",
|
||||
status: "正常", iteratedAt: "2026-01-09", ranking: "相符", ks: 45.8, psi: 5.1,
|
||||
ksDrop: 2.2, secondaryHits: 0, wuji: false, processedAt: "2026-04-28",
|
||||
previousAdvice: "维持现状,继续监控", commonModel: "标准A卡通用版 v2.3",
|
||||
},
|
||||
{
|
||||
bank: "通汇银行", category: "std", name: "标准A卡", modelId: "TH-STD-001", version: "v2.0",
|
||||
status: "正常", iteratedAt: "2025-11-30", ranking: "不符", ks: 38.73, psi: 9.4,
|
||||
ksDrop: 11.8, secondaryHits: 4, wuji: false, processedAt: "2026-05-16",
|
||||
previousAdvice: "建议开展模型微调可行性评估", commonModel: "标准A卡通用版 v2.3",
|
||||
},
|
||||
{
|
||||
bank: "北岸银行", category: "std", name: "标准A卡", modelId: "BA-STD-001", version: "v1.9",
|
||||
status: "下线", iteratedAt: "2024-12-11", ranking: "—", ks: 0, psi: 0,
|
||||
ksDrop: 0, secondaryHits: 0, wuji: false, processedAt: "2026-03-02",
|
||||
previousAdvice: "模型已下线,不再参与月度监控", commonModel: null,
|
||||
},
|
||||
{
|
||||
bank: "江城银行", category: "big", name: "大额A卡", modelId: "JC-BIG-001", version: "v1.4",
|
||||
status: "正常", iteratedAt: "2026-07-03", ranking: "相符", ks: 46.3, psi: 4.8,
|
||||
ksDrop: 1.5, secondaryHits: 0, wuji: false, processedAt: "2026-06-24",
|
||||
previousAdvice: "维持现状,继续监控", commonModel: "大额A卡通用版 v1.4",
|
||||
},
|
||||
{
|
||||
bank: "华东银行", category: "big", name: "大额A卡", modelId: "HD-BIG-001", version: "v1.3",
|
||||
status: "正常", iteratedAt: "2026-04-16", ranking: "相符", ks: 44.9, psi: 7.3,
|
||||
ksDrop: 4.9, secondaryHits: 0, wuji: false, processedAt: "2026-05-30",
|
||||
previousAdvice: "维持现状,继续监控", commonModel: "大额A卡通用版 v1.4",
|
||||
},
|
||||
{
|
||||
bank: "云岭银行", category: "big", name: "大额A卡", modelId: "YL-BIG-001", version: "v1.2",
|
||||
status: "陪跑结束", iteratedAt: "2026-08-05", ranking: "相符", ks: 42.7, psi: 9.6,
|
||||
ksDrop: 8.2, secondaryHits: 0, wuji: false, processedAt: null,
|
||||
previousAdvice: "暂无历史处理建议", commonModel: "大额A卡通用版 v1.4",
|
||||
},
|
||||
{
|
||||
bank: "滨海银行", category: "big", name: "大额A卡", modelId: "BH-BIG-001", version: "v1.1",
|
||||
status: "正常", iteratedAt: "2025-09-25", ranking: "相符", ks: 39.4, psi: 11.2,
|
||||
ksDrop: 9.7, secondaryHits: 2, wuji: false, processedAt: "2026-04-19",
|
||||
previousAdvice: "建议持续跟踪,下期复核", commonModel: "大额A卡通用版 v1.4",
|
||||
},
|
||||
{
|
||||
bank: "南岭银行", category: "bai", name: "白户A卡", modelId: "NL-BAI-001", version: "v1.5",
|
||||
status: "正常", iteratedAt: "2026-05-22", ranking: "不符", ks: 34.2, psi: 18.7,
|
||||
ksDrop: 15.3, secondaryHits: 2, wuji: true, processedAt: "2026-06-09",
|
||||
previousAdvice: "建议启动模型微调或重构评估", commonModel: null,
|
||||
},
|
||||
{
|
||||
bank: "通汇银行", category: "bai", name: "白户A卡", modelId: "TH-BAI-001", version: "v1.4",
|
||||
status: "正常", iteratedAt: "2026-02-11", ranking: "相符", ks: 37.6, psi: 12.1,
|
||||
ksDrop: 7.4, secondaryHits: 1, wuji: false, processedAt: "2026-05-11",
|
||||
previousAdvice: "建议重点关注并持续跟踪", commonModel: "白户A卡通用版 v1.4",
|
||||
},
|
||||
{
|
||||
bank: "江城银行", category: "bai", name: "白户A卡", modelId: "JC-BAI-001", version: "v1.3",
|
||||
status: "正常", iteratedAt: "2025-12-05", ranking: "相符", ks: 40.8, psi: 9.9,
|
||||
ksDrop: 5.6, secondaryHits: 0, wuji: false, processedAt: "2026-03-26",
|
||||
previousAdvice: "维持现状,继续监控", commonModel: "白户A卡通用版 v1.4",
|
||||
},
|
||||
{
|
||||
bank: "华东银行", category: "afd", name: "反欺诈评分", modelId: "HD-AFD-001", version: "v2.0",
|
||||
status: "正常", iteratedAt: "2026-04-10", ranking: "不符", ks: 28.6, psi: 31.5,
|
||||
ksDrop: 18.9, secondaryHits: 3, wuji: false, processedAt: "2026-06-21",
|
||||
previousAdvice: "建议启动模型微调或重构评估", commonModel: null,
|
||||
},
|
||||
{
|
||||
bank: "滨海银行", category: "afd", name: "反欺诈评分", modelId: "BH-AFD-001", version: "v1.8",
|
||||
status: "正常", iteratedAt: "2025-10-30", ranking: "相符", ks: 36.4, psi: 8.1,
|
||||
ksDrop: 4.2, secondaryHits: 3, wuji: true, processedAt: "2026-05-25",
|
||||
previousAdvice: "建议重点关注并持续跟踪", commonModel: "反欺诈评分通用版 v1.8",
|
||||
},
|
||||
{
|
||||
bank: "云岭银行", category: "afd", name: "反欺诈评分", modelId: "YL-AFD-001", version: "v1.7",
|
||||
status: "正常", iteratedAt: "2025-08-19", ranking: "相符", ks: 38.2, psi: 14.6,
|
||||
ksDrop: 12.7, secondaryHits: 2, wuji: false, processedAt: "2026-04-15",
|
||||
previousAdvice: "建议持续跟踪,下期复核", commonModel: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const FEATURE_METRICS = [
|
||||
{ key: "age", name: "申请人年龄", iv: 0.284, ivDrop: 6.8, csi: 0.041, csiRise: 0.8, ksContribution: -0.7, psiContribution: 1.2 },
|
||||
{ key: "income", name: "月均收入", iv: 0.251, ivDrop: 12.4, csi: 0.058, csiRise: 1.7, ksContribution: -1.2, psiContribution: 2.4 },
|
||||
{ key: "debt_ratio", name: "负债收入比", iv: 0.219, ivDrop: 18.6, csi: 0.076, csiRise: 2.9, ksContribution: -2.1, psiContribution: 4.1 },
|
||||
{ key: "credit_age", name: "信贷账龄", iv: 0.193, ivDrop: 21.3, csi: 0.083, csiRise: 3.5, ksContribution: -2.8, psiContribution: 5.2 },
|
||||
{ key: "query_3m", name: "近三月查询次数", iv: 0.171, ivDrop: 24.7, csi: 0.091, csiRise: 4.2, ksContribution: -3.4, psiContribution: 6.8 },
|
||||
] 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: ["(0,580]", "[580,600)", "[600,620)", "[620,640)", "[640,660)", "[660,680)", "[680,+)"],
|
||||
counts: [307, 418, 730, 1100, 1545, 1895, 6104],
|
||||
badRates: [10.42, 5.98, 5.48, 3.64, 2.85, 1.64, 0.31],
|
||||
} as const;
|
||||
|
||||
export const BANK_AVERAGE_CYCLE: Record<string, number> = {
|
||||
江城银行: 8.6,
|
||||
滨海银行: 11.2,
|
||||
华东银行: 9.8,
|
||||
南岭银行: 12.4,
|
||||
云岭银行: 10.1,
|
||||
通汇银行: 13.6,
|
||||
北岸银行: 16.0,
|
||||
bins: [] as string[],
|
||||
counts: [] as number[],
|
||||
badRates: [] as number[],
|
||||
};
|
||||
export const BANK_AVERAGE_CYCLE: Record<string, number> = {};
|
||||
|
||||
export type ModelLifecycle = {
|
||||
onlineAt: string | null;
|
||||
@@ -183,25 +74,7 @@ export type ModelLifecycle = {
|
||||
maxLift: number;
|
||||
};
|
||||
|
||||
export const MODEL_LIFECYCLE: Record<string, ModelLifecycle> = {
|
||||
"JC-STD-001": { onlineAt: "2025-03-12", escortStartAt: "2025-01-20", escortEndAt: "2025-03-10", offlineAt: null, developer: "李伟", developmentKs: 46.0, developmentPsi: 3.92, maxLift: 3.24 },
|
||||
"BH-STD-001": { onlineAt: null, escortStartAt: "2026-07-28", escortEndAt: null, offlineAt: null, developer: "王芳", developmentKs: 47.5, developmentPsi: 3.48, maxLift: 3.37 },
|
||||
"HD-STD-001": { onlineAt: "2024-11-05", escortStartAt: "2024-09-18", escortEndAt: "2024-11-01", offlineAt: null, developer: "李伟", developmentKs: 46.9, developmentPsi: 3.71, maxLift: 3.18 },
|
||||
"NL-STD-001": { onlineAt: "2025-08-22", escortStartAt: "2025-06-30", escortEndAt: "2025-08-18", offlineAt: null, developer: "王芳", developmentKs: 45.4, developmentPsi: 4.06, maxLift: 3.09 },
|
||||
"YL-STD-001": { onlineAt: "2025-05-14", escortStartAt: "2025-03-22", escortEndAt: "2025-05-10", offlineAt: null, developer: "李伟", developmentKs: 48.2, developmentPsi: 3.26, maxLift: 3.42 },
|
||||
"TH-STD-001": { onlineAt: "2024-06-18", escortStartAt: "2024-04-25", escortEndAt: "2024-06-14", offlineAt: null, developer: "王芳", developmentKs: 44.8, developmentPsi: 4.31, maxLift: 2.96 },
|
||||
"BA-STD-001": { onlineAt: "2023-09-01", escortStartAt: null, escortEndAt: null, offlineAt: "2026-02-28", developer: "李伟", developmentKs: 42.7, developmentPsi: 4.62, maxLift: 2.81 },
|
||||
"JC-BIG-001": { onlineAt: "2025-10-08", escortStartAt: "2025-08-15", escortEndAt: "2025-10-05", offlineAt: null, developer: "王芳", developmentKs: 49.1, developmentPsi: 3.05, maxLift: 3.55 },
|
||||
"HD-BIG-001": { onlineAt: "2025-02-19", escortStartAt: "2024-12-20", escortEndAt: "2025-02-15", offlineAt: null, developer: "李伟", developmentKs: 47.8, developmentPsi: 3.37, maxLift: 3.31 },
|
||||
"YL-BIG-001": { onlineAt: "2026-08-05", escortStartAt: "2026-06-10", escortEndAt: "2026-08-01", offlineAt: null, developer: "王芳", developmentKs: 46.6, developmentPsi: 3.68, maxLift: 3.12 },
|
||||
"BH-BIG-001": { onlineAt: "2024-12-03", escortStartAt: "2024-10-11", escortEndAt: "2024-11-29", offlineAt: null, developer: "李伟", developmentKs: 43.9, developmentPsi: 4.25, maxLift: 2.91 },
|
||||
"NL-BAI-001": { onlineAt: "2025-07-16", escortStartAt: "2025-05-20", escortEndAt: "2025-07-12", offlineAt: null, developer: "王芳", developmentKs: 42.6, developmentPsi: 4.74, maxLift: 2.73 },
|
||||
"TH-BAI-001": { onlineAt: "2025-04-09", escortStartAt: "2025-02-14", escortEndAt: "2025-04-05", offlineAt: null, developer: "李伟", developmentKs: 43.7, developmentPsi: 4.18, maxLift: 2.88 },
|
||||
"JC-BAI-001": { onlineAt: "2024-08-27", escortStartAt: "2024-07-01", escortEndAt: "2024-08-23", offlineAt: null, developer: "王芳", developmentKs: 45.1, developmentPsi: 3.89, maxLift: 3.03 },
|
||||
"HD-AFD-001": { onlineAt: "2025-01-22", escortStartAt: "2024-11-28", escortEndAt: "2025-01-18", offlineAt: null, developer: "李伟", developmentKs: 40.8, developmentPsi: 5.16, maxLift: 2.54 },
|
||||
"BH-AFD-001": { onlineAt: "2024-10-14", escortStartAt: "2024-08-20", escortEndAt: "2024-10-10", offlineAt: null, developer: "王芳", developmentKs: 42.3, developmentPsi: 4.69, maxLift: 2.68 },
|
||||
"YL-AFD-001": { onlineAt: "2024-05-08", escortStartAt: "2024-03-15", escortEndAt: "2024-05-04", offlineAt: null, developer: "李伟", developmentKs: 43.5, developmentPsi: 4.42, maxLift: 2.79 },
|
||||
};
|
||||
export const MODEL_LIFECYCLE: Record<string, ModelLifecycle> = {};
|
||||
|
||||
export function categoryName(category: ModelCategoryId): string {
|
||||
return MODEL_CATEGORIES.find((item) => item.id === category)?.name ?? category;
|
||||
@@ -252,55 +125,16 @@ export function abnormalReasonOf(model: ModelRecord | MonitoringRow): string {
|
||||
return reasons.join("、");
|
||||
}
|
||||
|
||||
function hash(value: string): number {
|
||||
let result = 0;
|
||||
for (const character of value) result = (result * 31 + character.charCodeAt(0)) >>> 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function modelTrend(
|
||||
model: ModelRecord,
|
||||
metric: "ks" | "psi",
|
||||
months: readonly string[] = MONITOR_MONTHS,
|
||||
_model: ModelRecord,
|
||||
_metric: "ks" | "psi",
|
||||
_months: readonly string[] = MONITOR_MONTHS,
|
||||
): number[] {
|
||||
const end = model[metric];
|
||||
const amplitude = metric === "ks" ? 2.6 : 4.4;
|
||||
const seed = hash(`${model.modelId}-${metric}`);
|
||||
const waveAt = (index: number) => (((seed >> (index % 12)) % 9) - 4) * (amplitude / 12);
|
||||
const lastWave = waveAt(Math.max(0, months.length - 1));
|
||||
return months.map((_, index) => {
|
||||
const distance = months.length - 1 - index;
|
||||
const drift = metric === "ks" ? distance * 0.32 : -distance * 0.74;
|
||||
const wave = waveAt(index) - lastWave;
|
||||
return Math.max(0, Number((end + drift + wave).toFixed(2)));
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
export function monitoringRows(source: ModelRecord[] = MODELS): MonitoringRow[] {
|
||||
const rows: MonitoringRow[] = [];
|
||||
for (const model of source) {
|
||||
if (model.status === "下线") {
|
||||
rows.push({ ...model, monitorMonth: "2026-02", ranking: "相符", ks: 39.8, psi: 9.1, ksDrop: 6.2, secondaryHits: 1 });
|
||||
continue;
|
||||
}
|
||||
const ksTrend = modelTrend(model, "ks");
|
||||
const psiTrend = modelTrend(model, "psi");
|
||||
MONITOR_MONTHS.forEach((monitorMonth, index) => {
|
||||
rows.push({
|
||||
...model,
|
||||
monitorMonth,
|
||||
ks: ksTrend[index] ?? model.ks,
|
||||
psi: psiTrend[index] ?? model.psi,
|
||||
ksDrop: Math.max(0, Number((model.ksDrop - (MONITOR_MONTHS.length - 1 - index) * 0.9).toFixed(2))),
|
||||
secondaryHits: Math.max(0, model.secondaryHits - (MONITOR_MONTHS.length - 1 - index)),
|
||||
});
|
||||
});
|
||||
}
|
||||
return rows.sort((left, right) => (
|
||||
right.monitorMonth.localeCompare(left.monitorMonth)
|
||||
|| left.bank.localeCompare(right.bank, "zh-CN")
|
||||
|| left.modelId.localeCompare(right.modelId)
|
||||
));
|
||||
export function monitoringRows(_source: ModelRecord[] = MODELS): MonitoringRow[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function monthsBetween(from: string, to: string): string[] {
|
||||
@@ -323,35 +157,29 @@ export function average(values: number[]): number {
|
||||
}
|
||||
|
||||
export function categoryTrend(
|
||||
category: ModelCategoryId,
|
||||
metric: "ks" | "psi",
|
||||
months: string[],
|
||||
source: ModelRecord[] = MODELS,
|
||||
_category: ModelCategoryId,
|
||||
_metric: "ks" | "psi",
|
||||
_months: string[],
|
||||
_source: ModelRecord[] = MODELS,
|
||||
): number[] {
|
||||
const models = source.filter((model) => model.category === category && model.status !== "下线");
|
||||
const series = models.map((model) => modelTrend(model, metric, months));
|
||||
return months.map((_, index) => Number(average(series.map((values) => values[index] ?? 0)).toFixed(2)));
|
||||
return [];
|
||||
}
|
||||
|
||||
export function bankTrend(
|
||||
bank: string,
|
||||
metric: "ks" | "psi",
|
||||
months: string[],
|
||||
source: ModelRecord[] = MODELS,
|
||||
_bank: string,
|
||||
_metric: "ks" | "psi",
|
||||
_months: string[],
|
||||
_source: ModelRecord[] = MODELS,
|
||||
): number[] {
|
||||
const models = source.filter((model) => model.bank === bank && model.status !== "下线");
|
||||
const series = models.map((model) => modelTrend(model, metric, months));
|
||||
return months.map((_, index) => Number(average(series.map((values) => values[index] ?? 0)).toFixed(2)));
|
||||
return [];
|
||||
}
|
||||
|
||||
export function modelsTrend(
|
||||
models: ModelRecord[],
|
||||
metric: "ks" | "psi",
|
||||
months: string[],
|
||||
_models: ModelRecord[],
|
||||
_metric: "ks" | "psi",
|
||||
_months: string[],
|
||||
): number[] {
|
||||
const activeModels = models.filter((model) => model.status !== "下线");
|
||||
const series = activeModels.map((model) => modelTrend(model, metric, months));
|
||||
return months.map((_, index) => Number(average(series.map((values) => values[index] ?? 0)).toFixed(2)));
|
||||
return [];
|
||||
}
|
||||
|
||||
export function latestIterationDate(models: ModelRecord[]): string {
|
||||
@@ -368,9 +196,9 @@ export function modelIterationCycleMonths(model: ModelRecord): number | null {
|
||||
return Math.max(0, (endYear * 12 + endMonth) - (startYear * 12 + startMonth));
|
||||
}
|
||||
|
||||
export function averageIterationCycle(models: ModelRecord[]): number {
|
||||
export function averageIterationCycle(models: ModelRecord[]): number | null {
|
||||
const values = models.map(modelIterationCycleMonths).filter((value): value is number => value !== null);
|
||||
return Number(average(values).toFixed(1));
|
||||
return values.length ? Number(average(values).toFixed(1)) : null;
|
||||
}
|
||||
|
||||
export function averageIterationMonths(models: ModelRecord[]): number {
|
||||
|
||||
Reference in New Issue
Block a user