feat: add compliant data connectors and lineage
- add authorized nine-source crawler connectors with security controls and packaged schemas - add the school internship contract to the vendor data standard center - add bidirectional campus data-to-report lineage with gap ownership and remediation Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
92515928d9
commit
8dff9847e3
+543
-38
@@ -231,6 +231,7 @@ const tasks = [
|
||||
|
||||
const sources = [
|
||||
{
|
||||
id: "regional-jobs",
|
||||
name: "区域招聘岗位基础包",
|
||||
type: "互联网岗位 · 标准数据包",
|
||||
category: "vendor",
|
||||
@@ -243,6 +244,7 @@ const sources = [
|
||||
status: "服务有效",
|
||||
records: "41,236",
|
||||
quality: "93.4%",
|
||||
analysisCompleteness: 98.8,
|
||||
updated: "2026-03-18",
|
||||
dataAsOf: "2026-03-17",
|
||||
nextUpdate: "2026-04-01",
|
||||
@@ -255,10 +257,13 @@ const sources = [
|
||||
threshold: "综合质量 ≥ 90;主键唯一率 100%",
|
||||
changeSummary: "新增 12,430 条,修改 846 条,失效 312 条;新增 AI 应用岗位分类",
|
||||
steward: "平台数据产品部",
|
||||
stewardContact: "林岚 · 数据产品经理",
|
||||
technicalOwner: "平台数据运维组",
|
||||
technicalContact: "平台数据包运维值班",
|
||||
action: "检查更新",
|
||||
},
|
||||
{
|
||||
id: "salary-company",
|
||||
name: "薪资与企业画像基础包",
|
||||
type: "互联网数据 · 增值数据包",
|
||||
category: "vendor",
|
||||
@@ -271,6 +276,7 @@ const sources = [
|
||||
status: "服务有效",
|
||||
records: "36,284",
|
||||
quality: "91.8%",
|
||||
analysisCompleteness: 100,
|
||||
updated: "2026-03-18",
|
||||
dataAsOf: "2026-03-17",
|
||||
nextUpdate: "2026-04-01",
|
||||
@@ -283,10 +289,13 @@ const sources = [
|
||||
threshold: "综合质量 ≥ 90;薪资合法率 ≥ 98%",
|
||||
changeSummary: "更新 2,186 条薪资样本,补充 326 家企业规模与融资阶段标签",
|
||||
steward: "平台数据产品部",
|
||||
stewardContact: "林岚 · 数据产品经理",
|
||||
technicalOwner: "平台数据运维组",
|
||||
technicalContact: "平台数据包运维值班",
|
||||
action: "检查更新",
|
||||
},
|
||||
{
|
||||
id: "internship",
|
||||
name: "实习管理平台",
|
||||
type: "校内系统 · API",
|
||||
category: "school",
|
||||
@@ -299,6 +308,7 @@ const sources = [
|
||||
status: "正常",
|
||||
records: "32,418",
|
||||
quality: "96.2%",
|
||||
analysisCompleteness: 100,
|
||||
updated: "今天 08:12",
|
||||
dataAsOf: "2026-03-21",
|
||||
nextUpdate: "明天 08:00",
|
||||
@@ -311,10 +321,13 @@ const sources = [
|
||||
threshold: "必填完整率 ≥ 95%;跨表关联率 ≥ 98%",
|
||||
changeSummary: "新增 186 条,修改 42 条;更新 31 家实习企业岗位信息",
|
||||
steward: "就业处",
|
||||
stewardContact: "赵晴 · 实习管理专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "立即同步",
|
||||
},
|
||||
{
|
||||
id: "employment",
|
||||
name: "就业管理系统",
|
||||
type: "校内系统 · 数据库视图",
|
||||
category: "school",
|
||||
@@ -327,6 +340,7 @@ const sources = [
|
||||
status: "字段缺失",
|
||||
records: "8,926",
|
||||
quality: "87.6%",
|
||||
analysisCompleteness: 91.2,
|
||||
updated: "今天 07:50",
|
||||
dataAsOf: "2026-03-21",
|
||||
nextUpdate: "异常处理后重试",
|
||||
@@ -339,11 +353,14 @@ const sources = [
|
||||
threshold: "专业代码合法率 100%;必填完整率 ≥ 95%",
|
||||
changeSummary: "新增 126 条,修改 43 条;86 条薪资记录进入隔离区,未覆盖当前生产版本",
|
||||
steward: "就业处",
|
||||
stewardContact: "赵晴 · 就业数据专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "处理异常",
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
id: "curriculum",
|
||||
name: "课程标准库",
|
||||
type: "校内文件 · 结构化导入",
|
||||
category: "school",
|
||||
@@ -356,6 +373,7 @@ const sources = [
|
||||
status: "正常",
|
||||
records: "1,428",
|
||||
quality: "95.1%",
|
||||
analysisCompleteness: 100,
|
||||
updated: "昨天 17:20",
|
||||
dataAsOf: "2026-03-20",
|
||||
nextUpdate: "2026-08-25",
|
||||
@@ -368,10 +386,13 @@ const sources = [
|
||||
threshold: "课程代码唯一率 100%;技能映射覆盖率 ≥ 80%",
|
||||
changeSummary: "导入 2026 春季课程快照;新增 38 门课程,修订 126 份课程标准",
|
||||
steward: "教务处",
|
||||
stewardContact: "陈璐 · 教务数据专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "立即同步",
|
||||
},
|
||||
{
|
||||
id: "enterprise-demand",
|
||||
name: "企业确认需求库",
|
||||
type: "企业提交 · 问卷/工作台",
|
||||
category: "enterprise",
|
||||
@@ -384,6 +405,7 @@ const sources = [
|
||||
status: "授权有效",
|
||||
records: "8,350",
|
||||
quality: "97.3%",
|
||||
analysisCompleteness: 98.5,
|
||||
updated: "昨天 20:08",
|
||||
dataAsOf: "2026-03-21",
|
||||
nextUpdate: "企业提交后实时入队",
|
||||
@@ -396,7 +418,9 @@ const sources = [
|
||||
threshold: "企业身份与授权关联率 100%;必填完整率 ≥ 95%",
|
||||
changeSummary: "新增 18 家企业确认、54 项岗位需求;12 项过期需求自动停用",
|
||||
steward: "产教融合中心",
|
||||
stewardContact: "王健 · 企业合作专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "查看回执",
|
||||
},
|
||||
];
|
||||
@@ -827,6 +851,7 @@ function syncClosedLoopCollections() {
|
||||
function saveAndRenderClosedLoop(message = "") {
|
||||
saveClosedLoopState();
|
||||
syncClosedLoopCollections();
|
||||
renderDataReportLineage();
|
||||
renderClosedLoop();
|
||||
renderTasks();
|
||||
renderProjects();
|
||||
@@ -1502,6 +1527,7 @@ const reports = [
|
||||
{
|
||||
id: "salary-2026-03",
|
||||
type: "industry",
|
||||
dataProfile: "salary",
|
||||
typeLabel: "产业专题",
|
||||
title: "2026届实习就业薪资竞争力分析",
|
||||
scope: "全校2026届学生",
|
||||
@@ -1824,6 +1850,150 @@ professionReportCatalog.forEach(([name, college, score, focus], index) => {
|
||||
});
|
||||
});
|
||||
|
||||
const reportDataProfiles = {
|
||||
school: {
|
||||
label: "校级综合分析",
|
||||
topics: ["区域岗位趋势", "课程技能覆盖", "实习就业成效", "企业需求验证"],
|
||||
dependencies: [
|
||||
{ sourceId: "regional-jobs", requirement: "required", purpose: "岗位规模、技能需求与区域趋势" },
|
||||
{ sourceId: "salary-company", requirement: "optional", purpose: "区域薪资基准与企业画像补充" },
|
||||
{ sourceId: "internship", requirement: "required", purpose: "学生实习岗位与过程成效" },
|
||||
{ sourceId: "employment", requirement: "required", purpose: "毕业去向、岗位与就业薪资" },
|
||||
{ sourceId: "curriculum", requirement: "required", purpose: "专业课程与技能培养供给" },
|
||||
{ sourceId: "enterprise-demand", requirement: "required", purpose: "合作企业正式确认的人才需求" },
|
||||
],
|
||||
},
|
||||
college: {
|
||||
label: "学院专业建设分析",
|
||||
topics: ["专业结构对比", "共享能力分析", "培养成效验证", "企业协同机会"],
|
||||
dependencies: [
|
||||
{ sourceId: "regional-jobs", requirement: "required", purpose: "学院相关岗位与技能趋势" },
|
||||
{ sourceId: "salary-company", requirement: "optional", purpose: "专业群薪资和企业结构参考" },
|
||||
{ sourceId: "internship", requirement: "optional", purpose: "学院实习岗位与实践反馈" },
|
||||
{ sourceId: "employment", requirement: "optional", purpose: "学院毕业去向与岗位匹配验证" },
|
||||
{ sourceId: "curriculum", requirement: "required", purpose: "跨专业课程与共享技能供给" },
|
||||
{ sourceId: "enterprise-demand", requirement: "required", purpose: "学院合作企业需求与项目机会" },
|
||||
],
|
||||
},
|
||||
profession: {
|
||||
label: "专业人才需求分析",
|
||||
topics: ["岗位技能需求", "课程能力映射", "学生达成验证", "企业需求复核"],
|
||||
dependencies: [
|
||||
{ sourceId: "regional-jobs", requirement: "required", purpose: "对口岗位、技能频次与需求变化" },
|
||||
{ sourceId: "salary-company", requirement: "optional", purpose: "区域同岗位薪资竞争力" },
|
||||
{ sourceId: "internship", requirement: "optional", purpose: "学生实践岗位与企业评价" },
|
||||
{ sourceId: "employment", requirement: "optional", purpose: "毕业去向和岗位匹配验证" },
|
||||
{ sourceId: "curriculum", requirement: "required", purpose: "课程标准、学时与技能覆盖" },
|
||||
{ sourceId: "enterprise-demand", requirement: "required", purpose: "真实企业需求与合作意向确认" },
|
||||
],
|
||||
},
|
||||
industry: {
|
||||
label: "产业人才缺口分析",
|
||||
topics: ["产业岗位趋势", "岗位技能归一", "培养供给对照", "企业确认校验"],
|
||||
dependencies: [
|
||||
{ sourceId: "regional-jobs", requirement: "required", purpose: "产业岗位规模、增速与技能信号" },
|
||||
{ sourceId: "salary-company", requirement: "optional", purpose: "重点产业薪资与企业结构" },
|
||||
{ sourceId: "curriculum", requirement: "required", purpose: "全校专业与课程技能供给" },
|
||||
{ sourceId: "enterprise-demand", requirement: "required", purpose: "合作企业需求交叉验证" },
|
||||
],
|
||||
},
|
||||
salary: {
|
||||
label: "实习就业薪资分析",
|
||||
topics: ["区域薪资基准", "实习薪资分布", "就业薪资对比", "专业竞争力"],
|
||||
dependencies: [
|
||||
{ sourceId: "regional-jobs", requirement: "optional", purpose: "区域岗位热度与样本辅助筛选" },
|
||||
{ sourceId: "salary-company", requirement: "required", purpose: "区域同岗位薪资基准" },
|
||||
{ sourceId: "internship", requirement: "required", purpose: "学生实习薪资与岗位信息" },
|
||||
{ sourceId: "employment", requirement: "required", purpose: "毕业生就业薪资与去向" },
|
||||
],
|
||||
},
|
||||
quality: {
|
||||
label: "数据质量与口径分析",
|
||||
topics: ["完整性与准确性", "版本与时效", "授权与权属", "加工血缘"],
|
||||
dependencies: [
|
||||
{ sourceId: "regional-jobs", requirement: "required", purpose: "岗位数据质量与归一覆盖" },
|
||||
{ sourceId: "salary-company", requirement: "required", purpose: "薪资和企业画像质量" },
|
||||
{ sourceId: "internship", requirement: "required", purpose: "实习数据完整性与时效" },
|
||||
{ sourceId: "employment", requirement: "required", purpose: "就业数据完整性与异常记录" },
|
||||
{ sourceId: "curriculum", requirement: "required", purpose: "课程数据版本与技能映射" },
|
||||
{ sourceId: "enterprise-demand", requirement: "required", purpose: "企业身份、授权与需求质量" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dataGapCatalog = [
|
||||
{
|
||||
id: "job-normalization",
|
||||
sourceId: "regional-jobs",
|
||||
level: "attention",
|
||||
levelLabel: "待复核",
|
||||
title: "新增岗位缺少标准岗位代码与技能映射",
|
||||
drawerTitle: "新增岗位名称待归一",
|
||||
drawerLevel: "需要复核",
|
||||
amount: "218 个岗位名称 · 影响 1,426 条记录",
|
||||
affected: "218 个岗位别名 · 影响 1,426 条招聘记录",
|
||||
fields: ["standard_position_code", "normalized_job_name", "skill_tags"],
|
||||
affectedProfileKeys: ["school", "college", "profession", "industry", "quality"],
|
||||
reason: "新增“具身智能应用工程师”等岗位未映射到当前岗位分类词典 V3.2。",
|
||||
action: "优先审核需求量前 50 的岗位名称,确认标准岗位、技能标签及别名规则。",
|
||||
status: "平台侧复核中",
|
||||
due: "2026-03-24",
|
||||
request: "请平台数据产品部确认标准岗位及技能标签,由平台数据运维组发布别名规则增量。",
|
||||
},
|
||||
{
|
||||
id: "salary-missing",
|
||||
sourceId: "employment",
|
||||
level: "blocking",
|
||||
levelLabel: "需补数",
|
||||
title: "就业薪资与缺失原因未完整提供",
|
||||
drawerTitle: "就业系统薪资字段缺失",
|
||||
drawerLevel: "阻断发布",
|
||||
amount: "86 条记录 · 当前字段完整率 91.2%",
|
||||
affected: "86 条记录 · 电子商务专业 41 条、现代服务类专业 45 条",
|
||||
fields: ["employment_salary", "salary_period", "missing_reason"],
|
||||
affectedReportIds: ["school-2026-03", "salary-2026-03", "quality-2026-03", "ecommerce-2026-03"],
|
||||
reason: "employment_salary 为空,未达到必填完整率 95% 的发布阈值。",
|
||||
action: "由就业处补齐薪资或填写缺失原因,修复文件按 student_id + cohort 回写后重新校验。",
|
||||
status: "待就业处补充",
|
||||
due: "2026-03-25",
|
||||
request: "请就业处补齐薪资或登记缺失原因,由学校信息中心回写只读视图并重新执行质量校验。",
|
||||
},
|
||||
{
|
||||
id: "enterprise-duplicate",
|
||||
sourceId: "enterprise-demand",
|
||||
level: "attention",
|
||||
levelLabel: "待核验",
|
||||
title: "企业主标识存在疑似重复",
|
||||
drawerTitle: "疑似重复企业记录",
|
||||
drawerLevel: "一般事项",
|
||||
amount: "12 家企业 · 涉及 37 项确认需求",
|
||||
affected: "12 家企业 · 涉及 37 项岗位需求",
|
||||
fields: ["enterprise_credit_code", "master_enterprise_id"],
|
||||
affectedProfileKeys: ["school", "college", "profession", "industry", "quality"],
|
||||
reason: "企业简称不同,但统一社会信用代码或工商地址高度相似。",
|
||||
action: "由产教融合中心确认主企业记录,合并需求与授权回执并保留审计映射。",
|
||||
status: "待产教融合中心确认",
|
||||
due: "2026-03-26",
|
||||
request: "请产教融合中心确认企业主记录,由学校信息中心合并需求与授权回执并保留审计映射。",
|
||||
},
|
||||
];
|
||||
|
||||
const sourceAnalysisTopics = {
|
||||
"regional-jobs": ["岗位需求趋势", "岗位技能归一", "产业增长信号"],
|
||||
"salary-company": ["区域薪资基准", "企业规模画像", "岗位竞争力"],
|
||||
internship: ["实习岗位分布", "实践过程成效", "企业评价"],
|
||||
employment: ["毕业去向", "就业薪资", "岗位匹配成效"],
|
||||
curriculum: ["课程技能覆盖", "专业培养供给", "课程版本"],
|
||||
"enterprise-demand": ["企业需求确认", "校企合作机会", "授权有效性"],
|
||||
};
|
||||
|
||||
const dataLineageState = {
|
||||
mode: "report",
|
||||
selectedReportId: "school-2026-03",
|
||||
selectedSourceId: "employment",
|
||||
gapsOnly: false,
|
||||
};
|
||||
|
||||
const iconPaths = {
|
||||
course: '<path d="M4 5h16v14H4V5Zm4 0V3h8v2M8 10h8m-8 4h5"/>',
|
||||
lab: '<path d="M9 3h6m-5 0v6l-5 9a2 2 0 0 0 2 3h10a2 2 0 0 0 2-3l-5-9V3M8 15h8"/>',
|
||||
@@ -1983,6 +2153,7 @@ function applyRole(role, options = {}) {
|
||||
]);
|
||||
}
|
||||
|
||||
renderDataReportLineage();
|
||||
renderClosedLoop();
|
||||
if (document.getElementById("professionSelect")) {
|
||||
renderProfessionProfile(document.getElementById("professionSelect").value);
|
||||
@@ -2522,6 +2693,359 @@ function renderTasks(filter = "all", search = "") {
|
||||
}
|
||||
}
|
||||
|
||||
function reportDataProfileKey(report) {
|
||||
return report.dataProfile || report.type;
|
||||
}
|
||||
|
||||
function reportDataProfile(report) {
|
||||
return reportDataProfiles[reportDataProfileKey(report)] || reportDataProfiles.profession;
|
||||
}
|
||||
|
||||
function sourceById(sourceId) {
|
||||
return sources.find((source) => source.id === sourceId);
|
||||
}
|
||||
|
||||
function gapsForSource(sourceId) {
|
||||
return dataGapCatalog.filter((gap) => gap.sourceId === sourceId);
|
||||
}
|
||||
|
||||
function dataGapById(gapId) {
|
||||
return dataGapCatalog.find((gap) => gap.id === gapId);
|
||||
}
|
||||
|
||||
function dependencyForReport(report, sourceId) {
|
||||
return reportDataProfile(report).dependencies.find((dependency) => dependency.sourceId === sourceId);
|
||||
}
|
||||
|
||||
function reportsUsingSource(sourceId) {
|
||||
return reports.filter((report) => Boolean(dependencyForReport(report, sourceId)));
|
||||
}
|
||||
|
||||
function gapAffectsReport(gap, report) {
|
||||
if (!dependencyForReport(report, gap.sourceId)) return false;
|
||||
if (gap.affectedReportIds) return gap.affectedReportIds.includes(report.id);
|
||||
if (report.periodKey !== "current") return false;
|
||||
return gap.affectedProfileKeys?.includes(reportDataProfileKey(report)) ?? false;
|
||||
}
|
||||
|
||||
function reportsAffectedByGap(gap) {
|
||||
return reports.filter((report) => gapAffectsReport(gap, report));
|
||||
}
|
||||
|
||||
function reportReadiness(report) {
|
||||
const requiredDependencies = reportDataProfile(report).dependencies.filter(
|
||||
(dependency) => dependency.requirement === "required",
|
||||
);
|
||||
if (!requiredDependencies.length) return 100;
|
||||
return (
|
||||
requiredDependencies.reduce(
|
||||
(total, dependency) => total + sourceById(dependency.sourceId).analysisCompleteness,
|
||||
0,
|
||||
) /
|
||||
requiredDependencies.length
|
||||
);
|
||||
}
|
||||
|
||||
function reportGaps(report) {
|
||||
return dataGapCatalog.filter((gap) => gapAffectsReport(gap, report));
|
||||
}
|
||||
|
||||
function reportsGroupedByType(reportItems) {
|
||||
return reportItems.reduce((groups, report) => {
|
||||
const group = groups.get(report.type) || {
|
||||
type: report.type,
|
||||
label: report.typeLabel,
|
||||
reports: [],
|
||||
};
|
||||
group.reports.push(report);
|
||||
groups.set(report.type, group);
|
||||
return groups;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
function readableReportIdSet() {
|
||||
return new Set(reportsForCurrentRole().map((report) => report.id));
|
||||
}
|
||||
|
||||
function lineageEmptyNode(message) {
|
||||
return `<div class="lineage-empty-node">${svgIcon("database")}<strong>${message}</strong><small>关闭“只看缺口”可查看完整依赖。</small></div>`;
|
||||
}
|
||||
|
||||
function renderLineageSourceNode(source, dependency, selected = false, relevantGaps = gapsForSource(source.id)) {
|
||||
const gaps = relevantGaps.filter((gap) => gap.sourceId === source.id);
|
||||
const hasGap = gaps.length > 0;
|
||||
const requirement = dependency?.requirement || "required";
|
||||
const completeness = source.analysisCompleteness;
|
||||
return `
|
||||
<button class="lineage-node lineage-source-node ${hasGap ? "has-gap" : ""} ${selected ? "selected" : ""}" data-source-detail="${source.name}">
|
||||
<span class="lineage-node-top">
|
||||
<em class="${requirement}">${requirement === "required" ? "必需数据" : "可选增强"}</em>
|
||||
<i class="${hasGap ? "gap" : "ready"}">${hasGap ? gaps[0].levelLabel : "可用"}</i>
|
||||
</span>
|
||||
<strong>${source.name}</strong>
|
||||
<small>${dependency?.purpose || source.type}</small>
|
||||
<span class="lineage-node-meta">
|
||||
<span><b>${completeness.toFixed(1)}%</b> 完整</span>
|
||||
<span>${source.steward}</span>
|
||||
</span>
|
||||
${hasGap ? `<span class="lineage-node-gap">${gaps.map((gap) => gap.fields.slice(0, 2).join("、")).join(";")}</span>` : ""}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageTopicNode(topic, index) {
|
||||
return `
|
||||
<div class="lineage-topic-node">
|
||||
<span>${String(index + 1).padStart(2, "0")}</span>
|
||||
<strong>${topic}</strong>
|
||||
<small>统一口径计算 · 保留证据定位</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageReportNode(report, readableIds = readableReportIdSet()) {
|
||||
const gaps = reportGaps(report);
|
||||
const readiness = reportReadiness(report);
|
||||
const canRead = readableIds.has(report.id);
|
||||
return `
|
||||
<button class="lineage-node lineage-report-node ${gaps.length ? "has-gap" : ""}" data-lineage-report="${report.id}">
|
||||
<span class="lineage-node-top">
|
||||
<em>${report.typeLabel}</em>
|
||||
<i class="${gaps.length ? "gap" : "ready"}">${gaps.length ? `${gaps.length}项缺口` : "数据齐备"}</i>
|
||||
</span>
|
||||
<strong>${report.title}</strong>
|
||||
<small>${report.scope}</small>
|
||||
<span class="lineage-node-meta">
|
||||
<span><b>${readiness.toFixed(1)}%</b> 完备</span>
|
||||
<span>${canRead ? report.version : "仅依赖元数据"}</span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageReportGroup(group, readableIds) {
|
||||
const affectedCount = group.reports.filter((report) => reportGaps(report).length).length;
|
||||
return `
|
||||
<details class="lineage-report-group" ${group.reports.length <= 5 ? "open" : ""}>
|
||||
<summary>
|
||||
<span><strong>${group.label}</strong><small>${affectedCount ? `${affectedCount} 份存在数据缺口` : "当前数据关系可用"}</small></span>
|
||||
<em>${group.reports.length} 份</em>
|
||||
</summary>
|
||||
<div class="lineage-report-group-list">
|
||||
${group.reports.map((report) => renderLineageReportNode(report, readableIds)).join("")}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageStage(title, countText, className, content) {
|
||||
return `
|
||||
<section class="lineage-stage ${className}">
|
||||
<div class="lineage-stage-head"><span>${title}</span><strong>${countText}</strong></div>
|
||||
<div class="lineage-stage-list">${content}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function lineageConnector(label) {
|
||||
return `
|
||||
<div class="lineage-connector" aria-hidden="true">
|
||||
<span>${label}</span>
|
||||
<i><b></b></i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderReportLineageCanvas(report) {
|
||||
const profile = reportDataProfile(report);
|
||||
const gaps = reportGaps(report);
|
||||
const readableIds = readableReportIdSet();
|
||||
const gapSourceIds = new Set(gaps.map((gap) => gap.sourceId));
|
||||
const visibleDependencies = dataLineageState.gapsOnly
|
||||
? profile.dependencies.filter((dependency) => gapSourceIds.has(dependency.sourceId))
|
||||
: profile.dependencies;
|
||||
const sourceNodes =
|
||||
visibleDependencies
|
||||
.map((dependency) => renderLineageSourceNode(sourceById(dependency.sourceId), dependency, false, gaps))
|
||||
.join("") || lineageEmptyNode("当前报告没有数据缺口");
|
||||
const topicNodes = profile.topics.map(renderLineageTopicNode).join("");
|
||||
return `
|
||||
<div class="lineage-stage-grid report-direction">
|
||||
${renderLineageStage("数据资产", `${visibleDependencies.length} / ${profile.dependencies.length} 项`, "sources", sourceNodes)}
|
||||
${lineageConnector("形成指标")}
|
||||
${renderLineageStage("指标 / 分析主题", `${profile.topics.length} 类`, "topics", topicNodes)}
|
||||
${lineageConnector("生成报告")}
|
||||
${renderLineageStage("目标报告", "1 份", "reports", renderLineageReportNode(report, readableIds))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSourceLineageCanvas(source) {
|
||||
const sourceReports = reportsUsingSource(source.id);
|
||||
const affectedReportIds = new Set(
|
||||
gapsForSource(source.id).flatMap((gap) => reportsAffectedByGap(gap).map((report) => report.id)),
|
||||
);
|
||||
const visibleReports = dataLineageState.gapsOnly
|
||||
? sourceReports.filter((report) => affectedReportIds.has(report.id))
|
||||
: sourceReports;
|
||||
const reportGroups = [...reportsGroupedByType(visibleReports).values()];
|
||||
const readableIds = readableReportIdSet();
|
||||
const reportNodes =
|
||||
reportGroups
|
||||
.map((group) => renderLineageReportGroup(group, readableIds))
|
||||
.join("") || lineageEmptyNode("当前数据来源没有待处理的报告关系");
|
||||
const topics = sourceAnalysisTopics[source.id] || ["基础指标", "专题分析", "质量校验"];
|
||||
return `
|
||||
<div class="lineage-stage-grid source-direction">
|
||||
${renderLineageStage("选中数据", "1 项", "sources", renderLineageSourceNode(source, null, true))}
|
||||
${lineageConnector("支撑分析")}
|
||||
${renderLineageStage("可形成的分析", `${topics.length} 类`, "topics", topics.map(renderLineageTopicNode).join(""))}
|
||||
${lineageConnector("支撑报告")}
|
||||
${renderLineageStage("可形成的报告", `${visibleReports.length} 份`, "reports", reportNodes)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function lineageSummaryCard(label, value, note, className = "") {
|
||||
return `<article class="${className}"><span>${label}</span><strong>${value}</strong><small>${note}</small></article>`;
|
||||
}
|
||||
|
||||
function renderLineageSummary(report, source) {
|
||||
const summary = document.getElementById("dataLineageSummary");
|
||||
if (dataLineageState.mode === "report") {
|
||||
const profile = reportDataProfile(report);
|
||||
const requiredCount = profile.dependencies.filter((dependency) => dependency.requirement === "required").length;
|
||||
const gaps = reportGaps(report);
|
||||
summary.innerHTML = [
|
||||
lineageSummaryCard("当前报告", report.typeLabel, report.version),
|
||||
lineageSummaryCard("依赖数据", `${requiredCount} 必需 · ${profile.dependencies.length - requiredCount} 可选`, "依赖关系已登记"),
|
||||
lineageSummaryCard("数据完备度", `${reportReadiness(report).toFixed(1)}%`, gaps.length ? "使用稳定版本,可继续分析" : "达到生成条件"),
|
||||
lineageSummaryCard("待补数据", `${gaps.length} 项`, gaps.length ? "已定位责任人与时限" : "当前无数据缺口", gaps.length ? "warning" : "ready"),
|
||||
].join("");
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceReports = reportsUsingSource(source.id);
|
||||
const gaps = gapsForSource(source.id);
|
||||
summary.innerHTML = [
|
||||
lineageSummaryCard("当前数据", source.categoryLabel, source.version),
|
||||
lineageSummaryCard("支撑报告", `${sourceReports.length} 份`, `${reportsGroupedByType(sourceReports).size} 类报告`),
|
||||
lineageSummaryCard("当前质量", source.quality, `${source.records} 条有效记录`),
|
||||
lineageSummaryCard("数据缺口", `${gaps.length} 项`, gaps.length ? "已定位责任人与时限" : "当前无数据缺口", gaps.length ? "warning" : "ready"),
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderLineageGapCard(gap, selectedReport) {
|
||||
const source = sourceById(gap.sourceId);
|
||||
const impactCount = selectedReport ? 1 : reportsAffectedByGap(gap).length;
|
||||
return `
|
||||
<article class="lineage-gap-card ${gap.level}">
|
||||
<div class="lineage-gap-card-head">
|
||||
<span><i></i>${gap.levelLabel}</span>
|
||||
<em>${gap.status}</em>
|
||||
</div>
|
||||
<strong>${gap.title}</strong>
|
||||
<small>${source.name} · ${gap.amount}</small>
|
||||
<div class="lineage-gap-fields">${gap.fields.map((field) => `<span>${field}</span>`).join("")}</div>
|
||||
<p>${gap.request}</p>
|
||||
<dl>
|
||||
<div><dt>业务索取人</dt><dd>${source.stewardContact}</dd></div>
|
||||
<div><dt>技术接口人</dt><dd>${source.technicalContact}</dd></div>
|
||||
<div><dt>要求完成</dt><dd>${gap.due}</dd></div>
|
||||
<div><dt>影响范围</dt><dd>${selectedReport ? "当前报告" : `${impactCount} 份报告`}</dd></div>
|
||||
</dl>
|
||||
<button data-lineage-gap-action="${gap.id}">进入补数与质量处置</button>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageGaps(report, source) {
|
||||
const gaps = dataLineageState.mode === "report" ? reportGaps(report) : gapsForSource(source.id);
|
||||
document.getElementById("dataLineageGapCount").textContent = `${gaps.length} 项待处理`;
|
||||
document.getElementById("dataLineageGapList").innerHTML = gaps.length
|
||||
? gaps.map((gap) => renderLineageGapCard(gap, dataLineageState.mode === "report" ? report : null)).join("")
|
||||
: `
|
||||
<div class="lineage-gap-empty">
|
||||
${svgIcon("database")}
|
||||
<span><strong>当前关系没有待补数据</strong><small>数据版本、质量和授权均达到报告生成要求。</small></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDataReportLineage() {
|
||||
const selector = document.getElementById("dataLineageSelector");
|
||||
if (!selector) return;
|
||||
const mode = dataLineageState.mode;
|
||||
const readableReportIds = new Set(reportsForCurrentRole().map((report) => report.id));
|
||||
if (!reports.some((report) => report.id === dataLineageState.selectedReportId)) {
|
||||
dataLineageState.selectedReportId = "school-2026-03";
|
||||
}
|
||||
if (!sources.some((source) => source.id === dataLineageState.selectedSourceId)) {
|
||||
dataLineageState.selectedSourceId = sources[0].id;
|
||||
}
|
||||
const report = reports.find((item) => item.id === dataLineageState.selectedReportId) || reports[0];
|
||||
const source = sourceById(dataLineageState.selectedSourceId) || sources[0];
|
||||
|
||||
document.querySelectorAll("[data-lineage-mode]").forEach((button) => {
|
||||
const selected = button.dataset.lineageMode === mode;
|
||||
button.classList.toggle("active", selected);
|
||||
button.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
document.getElementById("dataLineageSelectorLabel").textContent = mode === "report" ? "选择报告" : "选择数据";
|
||||
selector.setAttribute("aria-label", mode === "report" ? "选择要追溯的报告" : "选择要反查的数据");
|
||||
selector.innerHTML =
|
||||
mode === "report"
|
||||
? reports
|
||||
.map((item) => {
|
||||
const canRead = readableReportIds.has(item.id);
|
||||
return `<option value="${item.id}">${item.typeLabel} · ${item.title}${canRead ? "" : " · 仅依赖元数据"}</option>`;
|
||||
})
|
||||
.join("")
|
||||
: sources
|
||||
.map((item) => `<option value="${item.id}">${item.categoryLabel} · ${item.name}</option>`)
|
||||
.join("");
|
||||
selector.value = mode === "report" ? report.id : source.id;
|
||||
document.getElementById("dataLineageGapToggle").checked = dataLineageState.gapsOnly;
|
||||
document.getElementById("dataLineageCanvas").innerHTML =
|
||||
mode === "report" ? renderReportLineageCanvas(report) : renderSourceLineageCanvas(source);
|
||||
renderLineageSummary(report, source);
|
||||
renderLineageGaps(report, source);
|
||||
}
|
||||
|
||||
function setupDataReportLineage() {
|
||||
const lineage = document.getElementById("dataReportLineage");
|
||||
lineage.addEventListener("click", (event) => {
|
||||
const modeButton = event.target.closest("[data-lineage-mode]");
|
||||
if (modeButton) {
|
||||
dataLineageState.mode = modeButton.dataset.lineageMode;
|
||||
renderDataReportLineage();
|
||||
return;
|
||||
}
|
||||
const reportButton = event.target.closest("[data-lineage-report]");
|
||||
if (reportButton) {
|
||||
const reportId = reportButton.dataset.lineageReport;
|
||||
if (!reportsForCurrentRole().some((report) => report.id === reportId)) {
|
||||
showToast("依赖元数据可见,但当前角色没有该报告正文的阅读权限。", "权限范围提示");
|
||||
return;
|
||||
}
|
||||
openReportReader(reportId);
|
||||
return;
|
||||
}
|
||||
const gapButton = event.target.closest("[data-lineage-gap-action]");
|
||||
if (gapButton) openDataOpsDrawer("issue", gapButton.dataset.lineageGapAction);
|
||||
});
|
||||
|
||||
document.getElementById("dataLineageSelector").addEventListener("change", (event) => {
|
||||
if (dataLineageState.mode === "report") dataLineageState.selectedReportId = event.target.value;
|
||||
else dataLineageState.selectedSourceId = event.target.value;
|
||||
renderDataReportLineage();
|
||||
});
|
||||
document.getElementById("dataLineageGapToggle").addEventListener("change", (event) => {
|
||||
dataLineageState.gapsOnly = event.target.checked;
|
||||
renderDataReportLineage();
|
||||
});
|
||||
}
|
||||
|
||||
const dataOpsState = {
|
||||
context: "source",
|
||||
sourceName: sources[0].name,
|
||||
@@ -2529,33 +3053,6 @@ const dataOpsState = {
|
||||
issue: null,
|
||||
};
|
||||
|
||||
const dataIssueCatalog = {
|
||||
"salary-missing": {
|
||||
title: "就业系统薪资字段缺失",
|
||||
level: "阻断发布",
|
||||
sourceName: "就业管理系统",
|
||||
affected: "86 条记录 · 电子商务专业 41 条、现代服务类专业 45 条",
|
||||
reason: "employment_salary 为空,未达到必填完整率 95% 的发布阈值。",
|
||||
action: "由就业处补齐薪资或填写缺失原因,修复文件按 student_id + cohort 回写后重新校验。",
|
||||
},
|
||||
"job-normalization": {
|
||||
title: "新增岗位名称待归一",
|
||||
level: "需要复核",
|
||||
sourceName: "区域招聘岗位基础包",
|
||||
affected: "218 个岗位别名 · 影响 1,426 条招聘记录",
|
||||
reason: "新增“具身智能应用工程师”等岗位未映射到当前岗位分类词典 V3.2。",
|
||||
action: "优先审核需求量前 50 的岗位名称,确认标准岗位、技能标签及别名规则。",
|
||||
},
|
||||
"enterprise-duplicate": {
|
||||
title: "疑似重复企业记录",
|
||||
level: "一般事项",
|
||||
sourceName: "企业确认需求库",
|
||||
affected: "12 家企业 · 涉及 37 项岗位需求",
|
||||
reason: "企业简称不同,但统一社会信用代码或工商地址高度相似。",
|
||||
action: "由产教融合中心确认主企业记录,合并需求与授权回执并保留审计映射。",
|
||||
},
|
||||
};
|
||||
|
||||
function currentDataSource() {
|
||||
return sources.find((source) => source.name === dataOpsState.sourceName) || sources[0];
|
||||
}
|
||||
@@ -2582,8 +3079,8 @@ function renderDataOpsOverview(source) {
|
||||
<h3>更新与责任</h3>
|
||||
<dl>
|
||||
<div><dt>数据类别</dt><dd>${source.categoryLabel} · ${source.commercial}</dd></div>
|
||||
<div><dt>业务责任人</dt><dd>${source.steward}</dd></div>
|
||||
<div><dt>技术责任人</dt><dd>${source.technicalOwner}</dd></div>
|
||||
<div><dt>业务责任人</dt><dd>${source.stewardContact} · ${source.steward}</dd></div>
|
||||
<div><dt>技术责任人</dt><dd>${source.technicalContact} · ${source.technicalOwner}</dd></div>
|
||||
<div><dt>更新模式</dt><dd>${source.updateMode}</dd></div>
|
||||
<div><dt>当前状态</dt><dd><span class="data-ops-state ${source.warning ? "warning" : ""}">${source.status}</span></dd></div>
|
||||
</dl>
|
||||
@@ -2674,7 +3171,7 @@ function renderDataOpsAudit(source) {
|
||||
return `
|
||||
<div class="data-authorization-card">
|
||||
<span>权属与授权</span><strong>${source.rights}</strong><p>${authorization}</p>
|
||||
<div><span>业务责任人<strong>${source.steward}</strong></span><span>技术责任人<strong>${source.technicalOwner}</strong></span><span>授权状态<strong>${source.status}</strong></span></div>
|
||||
<div><span>业务责任人<strong>${source.stewardContact} · ${source.steward}</strong></span><span>技术责任人<strong>${source.technicalContact} · ${source.technicalOwner}</strong></span><span>授权状态<strong>${source.status}</strong></span></div>
|
||||
</div>
|
||||
<article class="data-ops-card">
|
||||
<h3>最近审计事件</h3>
|
||||
@@ -2721,17 +3218,23 @@ function renderDataOpsUsage() {
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function renderDataIssue(issueKey) {
|
||||
const issue = dataIssueCatalog[issueKey];
|
||||
function renderDataIssue(issueId) {
|
||||
const issue = dataGapById(issueId);
|
||||
const source = currentDataSource();
|
||||
const impactCount = reportsAffectedByGap(issue).length;
|
||||
return `
|
||||
<div class="data-issue-hero">
|
||||
<span>${issue.level}</span><h3>${issue.title}</h3><p>${issue.affected}</p>
|
||||
<span>${issue.drawerLevel}</span><h3>${issue.drawerTitle}</h3><p>${issue.affected}</p>
|
||||
</div>
|
||||
<article class="data-ops-card"><h3>规则命中原因</h3><p>${issue.reason}</p></article>
|
||||
<article class="data-ops-card"><h3>建议处置</h3><p>${issue.action}</p></article>
|
||||
<article class="data-ops-card">
|
||||
<h3>受影响分析</h3>
|
||||
<div class="data-impact-grid"><span>当前生产版本<strong>未覆盖</strong></span><span>分析快照<strong>沿用稳定版本</strong></span><span>责任部门<strong>${currentDataSource().steward}</strong></span></div>
|
||||
<h3>受影响分析与补数责任</h3>
|
||||
<div class="data-impact-grid"><span>当前生产版本<strong>未覆盖</strong></span><span>影响报告<strong>${impactCount} 份</strong></span><span>分析快照<strong>沿用稳定版本</strong></span></div>
|
||||
<dl class="data-issue-owner-list">
|
||||
<div><dt>业务索取人</dt><dd>${source.stewardContact} · ${source.steward}</dd></div>
|
||||
<div><dt>技术接口人</dt><dd>${source.technicalContact} · ${source.technicalOwner}</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
@@ -2841,11 +3344,11 @@ function updateDataOpsDrawer() {
|
||||
}
|
||||
|
||||
if (dataOpsState.context === "issue") {
|
||||
const issue = dataIssueCatalog[dataOpsState.issue];
|
||||
const issueSource = sources.find((item) => item.name === issue.sourceName);
|
||||
const issue = dataGapById(dataOpsState.issue);
|
||||
const issueSource = sourceById(issue.sourceId);
|
||||
if (issueSource) dataOpsState.sourceName = issueSource.name;
|
||||
document.getElementById("dataOpsKicker").textContent = "DATA QUALITY WORKBENCH";
|
||||
document.getElementById("dataOpsTitle").textContent = issue.title;
|
||||
document.getElementById("dataOpsTitle").textContent = issue.drawerTitle;
|
||||
document.getElementById("dataOpsDescription").textContent = "查看受影响数据、规则命中原因和可审计的处理流程。";
|
||||
content.innerHTML = renderDataIssue(dataOpsState.issue);
|
||||
footer.innerHTML = `<button class="button secondary" data-data-ops-result="错误明细文件已准备,可交由责任部门修复">下载错误明细</button><button class="button primary" data-data-ops-result="已创建质量整改任务,并通知责任部门">创建整改任务</button>`;
|
||||
@@ -4707,6 +5210,7 @@ function init() {
|
||||
renderPartners();
|
||||
renderTasks();
|
||||
renderSources();
|
||||
renderDataReportLineage();
|
||||
setupGenericInteractions();
|
||||
setupRoleExperience();
|
||||
setupPlatformActions();
|
||||
@@ -4716,6 +5220,7 @@ function init() {
|
||||
setupForms();
|
||||
setupFilters();
|
||||
setupSourceFilters();
|
||||
setupDataReportLineage();
|
||||
setupDataOperations();
|
||||
setupWorkspaceDetails();
|
||||
setupProductExperience();
|
||||
|
||||
Reference in New Issue
Block a user