Files
edu-industry/admin-tools/demo/app.js
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 92515928d9 feat: add edu-industry platform demos
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-24 11:19:43 +08:00

3999 lines
176 KiB
JavaScript
Raw 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.
const sources = [
{
id: "boss",
name: "BOSS直聘",
short: "BOSS",
category: "commercial",
categoryLabel: "商业平台",
domain: "www.bosszhipin.com",
adapter: "BOSS-Collector 4.6",
mode: "网页/API混合采集",
schedule: "每 6 小时",
status: "running",
statusLabel: "正式运行",
health: 98.6,
today: 68420,
total: "4.86M",
updated: "10:42",
coverage: "全国 · 重点产业岗位",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-BOSS 2.5",
version: "SRC-BOSS-20260823-04",
owner: "商业源采集组",
retention: "原始响应 180 天 · 解析结果 36 个月",
rate: "自适应窗口 · 64 分片",
retry: "指数退避 · 最多 5 次",
change: "新增低空经济、具身智能岗位词表;更新城市筛选与增量水位。",
repo: "connector-registry/boss-collector",
codeVersion: "release/4.6.2 · 4f8d91a",
runtime: [
["10:42", "增量分片完成", "64/64 分片 · 68,420 条"],
["06:18", "原始快照固化", "RAW-BOSS-20260823-01 · SHA-256 已记录"],
["02:31", "解析规则发布", "PARSER-BOSS 4.6.2 · 回归样本 1,000 条"],
],
},
{
id: "zhaopin",
name: "智联招聘",
short: "智联",
category: "commercial",
categoryLabel: "商业平台",
domain: "www.zhaopin.com",
adapter: "ZHAOPIN-Collector 3.8",
mode: "网页/API混合采集",
schedule: "每 6 小时",
status: "running",
statusLabel: "正式运行",
health: 97.9,
today: 52360,
total: "3.92M",
updated: "10:38",
coverage: "全国 · 校招与社会招聘",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-ZL 2.2",
version: "SRC-ZL-20260823-04",
owner: "商业源采集组",
retention: "原始响应 180 天 · 解析结果 36 个月",
rate: "自适应窗口 · 48 分片",
retry: "指数退避 · 最多 5 次",
change: "补充企业性质与招聘人数映射;岗位有效期按源端状态增量维护。",
repo: "connector-registry/zhaopin-collector",
codeVersion: "release/3.8.1 · 96c2be7",
runtime: [
["10:38", "增量任务运行中", "44/48 分片 · 52,360 条"],
["06:05", "字段完整性预检", "完整率 98.8% · 通过"],
["02:16", "代码版本部署", "ZHAOPIN-Collector 3.8.1"],
],
},
{
id: "brzp",
name: "全国中小企业招聘平台",
short: "百日",
category: "public",
categoryLabel: "公共平台",
domain: "brzp.chinasme.cn",
adapter: "BRZP-Collector 2.1",
mode: "活动聚合与岗位采集",
schedule: "每日 01:30",
status: "running",
statusLabel: "正式运行",
health: 99.2,
today: 18640,
total: "824K",
updated: "09:55",
coverage: "全国中小企业 · 校招 · 实习",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-BRZP 1.4",
version: "SRC-BRZP-20260823-01",
owner: "公共源采集组",
retention: "原始页面 12 个月 · 解析结果 36 个月",
rate: "16 分片 · 活动页优先",
retry: "失败分片次日补采",
change: "同步全国中小企业招聘活动、地方专区、校园招聘和实习机会。",
repo: "connector-registry/brzp-collector",
codeVersion: "release/2.1.0 · c2d8f73",
runtime: [
["09:55", "活动专区补采完成", "新增活动 18 个 · 岗位 2,846 条"],
["03:08", "聚合来源校验", "9 个合作活动入口可访问"],
["01:42", "每日全量完成", "18,640 条 · 有效率 98.6%"],
],
},
{
id: "ncss",
name: "国家大学生就业服务平台",
short: "国就",
category: "public",
categoryLabel: "公共平台",
domain: "www.ncss.cn",
adapter: "NCSS-Collector 2.8",
mode: "职位与专场采集",
schedule: "每日 02:00",
status: "running",
statusLabel: "正式运行",
health: 99.4,
today: 14820,
total: "1.32M",
updated: "08:40",
coverage: "高校毕业生 · 校招 · 实习",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-NCSS 2.0",
version: "SRC-NCSS-20260823-01",
owner: "公共源采集组",
retention: "原始页面 12 个月 · 解析结果 36 个月",
rate: "24 分片 · 专场增量",
retry: "指数退避 · 最多 3 次",
change: "更新2026届国企岗位专题、联合招聘与实习岗位。",
repo: "connector-registry/ncss-collector",
codeVersion: "release/2.8.0 · 1a8d6e2",
runtime: [
["08:40", "职位增量完成", "14,820 条 · 1,286 家单位"],
["02:56", "专场活动同步", "新增 11 场联合招聘"],
["02:01", "调度任务启动", "24 个地域分片"],
],
},
{
id: "mohrss",
name: "中国公共招聘网",
short: "公招",
category: "public",
categoryLabel: "公共平台",
domain: "job.mohrss.gov.cn",
adapter: "MOHRSS-Collector 3.2",
mode: "公共岗位与招聘会采集",
schedule: "每日 02:30",
status: "running",
statusLabel: "正式运行",
health: 99.6,
today: 12680,
total: "1.86M",
updated: "08:26",
coverage: "全国公共就业岗位 · 重点行业",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-MOHRSS 2.3",
version: "SRC-MOHRSS-20260823-01",
owner: "公共源采集组",
retention: "原始页面 12 个月 · 解析结果 36 个月",
rate: "20 分片 · 地域增量",
retry: "失败分片次日补采",
change: "更新制造业招聘专区、名企招聘和中央企业招聘信息。",
repo: "connector-registry/mohrss-collector",
codeVersion: "release/3.2.4 · 66e102b",
runtime: [
["08:26", "增量任务完成", "12,680 条 · 有效率 99.1%"],
["03:12", "重点专区同步", "制造业与中央企业专区已更新"],
["02:31", "调度任务启动", "20 个地域分片"],
],
},
{
id: "iguopin",
name: "国聘",
short: "国聘",
category: "public",
categoryLabel: "公共平台",
domain: "www.iguopin.com",
adapter: "IGUOPIN-Collector 2.4",
mode: "国资央企岗位采集",
schedule: "每日 03:00",
status: "running",
statusLabel: "正式运行",
health: 98.8,
today: 9640,
total: "642K",
updated: "08:15",
coverage: "央企国企 · 校园招聘",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-GP 1.8",
version: "SRC-GP-20260823-01",
owner: "公共源采集组",
retention: "原始响应 12 个月 · 解析结果 36 个月",
rate: "12 分片 · 企业专题增量",
retry: "指数退避 · 最多 3 次",
change: "更新国资央企2026届岗位与重点企业专题。",
repo: "connector-registry/iguopin-collector",
codeVersion: "release/2.4.1 · a831bd0",
runtime: [
["08:15", "企业专题同步完成", "9,640 条 · 312 家企业"],
["03:44", "动态页面解析完成", "页面成功率 99.2%"],
["03:01", "调度任务启动", "12 个企业分片"],
],
},
{
id: "hzrc",
name: "杭州人才网",
short: "杭才",
category: "public",
categoryLabel: "公共平台",
domain: "www.hzrc.com",
adapter: "HZRC-Collector 2.7",
mode: "本地人才岗位采集",
schedule: "每 12 小时",
status: "attention",
statusLabel: "降速保护",
health: 92.4,
today: 3860,
total: "386K",
updated: "10:28",
coverage: "杭州都市圈 · 本地企业",
contract: "JOB-SIGNAL V3.0",
schema: "SOURCE-HZRC 2.0",
version: "SRC-HZRC-20260823-02",
owner: "区域源采集组",
retention: "原始页面 12 个月 · 解析结果 36 个月",
rate: "源端延迟保护 · 4 分片",
retry: "15分钟后自动重试",
change: "响应时延升高,采集器自动降低并发;已完成数据保持可用。",
repo: "connector-registry/hzrc-collector",
codeVersion: "release/2.7.3 · d09f71c",
runtime: [
["10:28", "触发自动降速", "P95 4.8s · 并发 12 → 4"],
["09:52", "增量任务继续", "3,860 条 · 8 分片待执行"],
["02:48", "全量索引校验", "岗位唯一率 99.7%"],
],
},
{
id: "school-employment",
name: "校内就业管理系统",
short: "就业",
category: "school",
categoryLabel: "校内系统",
domain: "school://employment-view",
adapter: "School-DB-Connector 2.3",
mode: "只读数据库视图",
schedule: "每日 04:00",
status: "running",
statusLabel: "正式运行",
health: 97.8,
today: 4280,
total: "928K",
updated: "07:52",
coverage: "36所学校 · 近三届毕业生",
contract: "SCHOOL-EMPLOYMENT V2.2",
schema: "EMPLOYMENT-VIEW 2.2",
version: "SCH-EMP-20260823-01",
owner: "学校数据接入组",
retention: "明细按租户策略 · 汇总 36 个月",
rate: "租户隔离 · 12 并行连接",
retry: "单租户失败隔离",
change: "新增就业质量、岗位相关度和去向确认字段。",
repo: "connector-registry/school-db-connector",
codeVersion: "release/2.3.0 · 310ae74",
runtime: [
["07:52", "36租户同步完成", "4,280 条变更 · 2 条隔离"],
["04:46", "跨表关联校验", "学生与专业关联率 99.6%"],
["04:01", "每日任务启动", "36 个租户分片"],
],
},
{
id: "school-internship",
name: "校内实习管理系统",
short: "实习",
category: "school",
categoryLabel: "校内系统",
domain: "school://internship-api",
adapter: "School-API-Connector 2.5",
mode: "租户 API 接入",
schedule: "每日 04:30",
status: "running",
statusLabel: "正式运行",
health: 98.4,
today: 5720,
total: "1.18M",
updated: "08:03",
coverage: "36所学校 · 实习全过程",
contract: "SCHOOL-INTERNSHIP V2.1",
schema: "INTERNSHIP-API 2.1",
version: "SCH-INT-20260823-01",
owner: "学校数据接入组",
retention: "明细按租户策略 · 汇总 36 个月",
rate: "租户隔离 · 12 并行连接",
retry: "单租户失败隔离",
change: "补充企业导师评价、实习工资与岗位任务字段。",
repo: "connector-registry/school-api-connector",
codeVersion: "release/2.5.2 · 8b17a63",
runtime: [
["08:03", "36租户同步完成", "5,720 条变更 · 全部通过"],
["05:10", "接口水位提交", "watermark 2026-08-23T04:30:00+08:00"],
["04:31", "每日任务启动", "36 个租户分片"],
],
},
];
const collectionJobs = [
{
id: "JOB-20260823-1042",
name: "BOSS重点产业岗位增量",
sourceId: "boss",
scope: "全国 · 数字技术/智能制造",
strategy: "增量 · 64分片 · 自适应",
progress: 68,
records: "68,420",
valid: "98.5%",
status: "running",
statusLabel: "运行中",
},
{
id: "JOB-20260823-1038",
name: "智联全国岗位增量",
sourceId: "zhaopin",
scope: "全国 · 校招与社招",
strategy: "增量 · 48分片 · 自适应",
progress: 91,
records: "52,360",
valid: "98.8%",
status: "running",
statusLabel: "运行中",
},
{
id: "JOB-20260823-1028",
name: "杭州人才网区域补采",
sourceId: "hzrc",
scope: "杭州都市圈 · 全岗位",
strategy: "补采 · 4分片 · 降速",
progress: 42,
records: "3,860",
valid: "96.4%",
status: "attention",
statusLabel: "降速保护",
},
{
id: "JOB-20260824-0130",
name: "百日招聘每日全量",
sourceId: "brzp",
scope: "全国 · 校招/实习/活动",
strategy: "全量 · 16分片",
progress: 0,
records: "—",
valid: "—",
status: "scheduled",
statusLabel: "等待调度",
},
{
id: "JOB-20260824-0200",
name: "国家大学生就业职位增量",
sourceId: "ncss",
scope: "全国 · 2026届",
strategy: "增量 · 24分片",
progress: 0,
records: "—",
valid: "—",
status: "scheduled",
statusLabel: "等待调度",
},
{
id: "JOB-20260824-0400",
name: "校内就业36租户同步",
sourceId: "school-employment",
scope: "36所学校 · 近三届",
strategy: "增量 · 租户隔离",
progress: 0,
records: "—",
valid: "—",
status: "scheduled",
statusLabel: "等待调度",
},
];
const standardJobRecords = [
{
id: "e5a87030-3c5f-4d74-a3ca-89427e2f75a1",
sourceId: "boss",
sourceRecordId: "BOSS-20260823-884201",
sourceUrl: "https://www.bosszhipin.com/job_detail/sample-884201.html",
taskId: "JOB-20260823-1042",
snapshotId: "RAW-BOSS-20260823-000884",
collectorVersion: "BOSS-Collector-4.6.2",
parserVersion: "PARSER-BOSS-4.6.2",
jobTitleRaw: "云原生开发工程师",
jobTitleNormalized: "云计算工程技术人员",
jobCode: "IT.CLOUD.DEV",
enterprise: "杭州示例数字科技有限公司",
region: "杭州",
regionCode: "330100",
workplace: "杭州市滨江区",
salaryMin: 12000,
salaryMax: 18000,
employmentType: "全职",
publishedAt: "2026-08-21 08:56",
collectedAt: "2026-08-23 10:42",
normalizedAt: "2026-08-23 10:45",
quality: 98.8,
status: "有效",
skills: ["Docker", "Kubernetes", "Java", "CI/CD"],
description: "负责容器平台应用开发、持续集成和服务可观测性建设。",
contentHash: "96c2be742dc43f8e7ee526db6358030873406c450f15e67225c605dc39b431a8",
payloadHash: "4f8d91a47e2b6d82b2aaad136d344d61c83f9cd472185d2543725c91870e9e32",
dataVersion: "V2026.08.23",
},
{
id: "57df68fe-2454-44d2-9f3d-20e91ac0912a",
sourceId: "zhaopin",
sourceRecordId: "ZL-20260823-620311",
sourceUrl: "https://www.zhaopin.com/jobdetail/sample-620311.htm",
taskId: "JOB-20260823-1038",
snapshotId: "RAW-ZHAOPIN-20260823-000620",
collectorVersion: "ZHAOPIN-Collector-3.8.1",
parserVersion: "PARSER-ZHAOPIN-3.8.1",
jobTitleRaw: "工业视觉应用工程师",
jobTitleNormalized: "人工智能工程技术人员",
jobCode: "AI.VISION.APP",
enterprise: "浙江示例智能装备股份有限公司",
region: "宁波",
regionCode: "330200",
workplace: "宁波市鄞州区",
salaryMin: 10000,
salaryMax: 16000,
employmentType: "全职",
publishedAt: "2026-08-22 14:05",
collectedAt: "2026-08-23 10:38",
normalizedAt: "2026-08-23 10:41",
quality: 98.6,
status: "有效",
skills: ["Python", "OpenCV", "机器视觉", "PLC"],
description: "负责机器视觉方案部署、相机标定、缺陷检测模型优化和产线集成。",
contentHash: "66e102b99bf056868670ac971011740c6f66f62b86ea43cc66326ed9a0a5807e",
payloadHash: "c2d8f733e6711219823110d7d35fdc5535f133ed60f60950f9dc78a228cb402d",
dataVersion: "V2026.08.23",
},
{
id: "62eaa80d-0b30-462b-8eca-c6c9bb5e8446",
sourceId: "brzp",
sourceRecordId: "BRZP-20260823-110028",
sourceUrl: "https://brzp.chinasme.cn/brzp/job/sample-110028",
taskId: "JOB-20260823-0955",
snapshotId: "RAW-BRZP-20260823-000110",
collectorVersion: "BRZP-Collector-2.1.0",
parserVersion: "PARSER-BRZP-2.1.0",
jobTitleRaw: "跨境电商运营实习生",
jobTitleNormalized: "电子商务师",
jobCode: "EC.CROSSBORDER.OPS",
enterprise: "浙江示例跨境服务有限公司",
region: "嘉兴",
regionCode: "330400",
workplace: "嘉兴市南湖区",
salaryMin: 3500,
salaryMax: 5000,
employmentType: "实习",
publishedAt: "2026-08-22 18:00",
collectedAt: "2026-08-23 09:55",
normalizedAt: "2026-08-23 10:00",
quality: 97.9,
status: "有效",
skills: ["跨境电商", "内容运营", "数据分析", "英语"],
description: "协助店铺运营、海外内容制作、经营数据分析与合规资料整理。",
contentHash: "310ae74d6d695235229da958f8fb8e59af6ea4bebef659dfdc1a6bc0764f3f30",
payloadHash: "a831bd0d4e29cab1e650f36a4fc9c5d5cc8053f560b5d3dc7aac62b384b47e11",
dataVersion: "V2026.08.23",
},
{
id: "84c7ac1a-2a5c-4bcc-b64a-fc9cdf42b081",
sourceId: "ncss",
sourceRecordId: "NCSS-20260823-381205",
sourceUrl: "https://www.ncss.cn/student/jobs/sample-381205.html",
taskId: "JOB-20260823-0840",
snapshotId: "RAW-NCSS-20260823-000381",
collectorVersion: "NCSS-Collector-2.8.0",
parserVersion: "PARSER-NCSS-2.8.0",
jobTitleRaw: "智能网联汽车测试工程师",
jobTitleNormalized: "汽车工程技术人员",
jobCode: "AUTO.ICV.TEST",
enterprise: "浙江示例汽车技术研究院",
region: "杭州",
regionCode: "330100",
workplace: "杭州市钱塘区",
salaryMin: 9000,
salaryMax: 14000,
employmentType: "校招",
publishedAt: "2026-08-22 10:18",
collectedAt: "2026-08-23 08:40",
normalizedAt: "2026-08-23 08:44",
quality: 99.1,
status: "有效",
skills: ["CAN", "自动驾驶", "测试开发", "Python"],
description: "承担智能网联汽车道路测试、数据采集、场景回放和测试报告编制。",
contentHash: "18f4".repeat(16),
payloadHash: "7ca2".repeat(16),
dataVersion: "V2026.08.23",
},
{
id: "3852086b-33d7-4ed5-9d90-52f1fd6f3a14",
sourceId: "mohrss",
sourceRecordId: "MOHRSS-20260823-226801",
sourceUrl: "https://job.mohrss.gov.cn/job/sample-226801",
taskId: "JOB-20260823-0826",
snapshotId: "RAW-MOHRSS-20260823-000226",
collectorVersion: "MOHRSS-Collector-3.2.4",
parserVersion: "PARSER-MOHRSS-3.2.4",
jobTitleRaw: "工业机器人系统运维员",
jobTitleNormalized: "工业机器人系统运维员",
jobCode: "MFG.ROBOT.OPS",
enterprise: "绍兴示例精密制造有限公司",
region: "绍兴",
regionCode: "330600",
workplace: "绍兴市柯桥区",
salaryMin: 8000,
salaryMax: 12000,
employmentType: "全职",
publishedAt: "2026-08-21 16:35",
collectedAt: "2026-08-23 08:26",
normalizedAt: "2026-08-23 08:30",
quality: 98.9,
status: "有效",
skills: ["工业机器人", "设备维护", "电气控制", "故障诊断"],
description: "负责工业机器人工作站巡检、预防性维护、故障诊断与备件管理。",
contentHash: "66a8".repeat(16),
payloadHash: "04de".repeat(16),
dataVersion: "V2026.08.23",
},
{
id: "c0f2185a-b718-4c47-82e5-21b082eeb2ed",
sourceId: "iguopin",
sourceRecordId: "GP-20260823-119640",
sourceUrl: "https://www.iguopin.com/job/detail/sample-119640",
taskId: "JOB-20260823-0815",
snapshotId: "RAW-IGUOPIN-20260823-000119",
collectorVersion: "IGUOPIN-Collector-2.4.1",
parserVersion: "PARSER-IGUOPIN-2.4.1",
jobTitleRaw: "数据治理工程师",
jobTitleNormalized: "数据工程技术人员",
jobCode: "IT.DATA.GOV",
enterprise: "华东示例产业数字集团",
region: "上海",
regionCode: "310000",
workplace: "上海市浦东新区",
salaryMin: 15000,
salaryMax: 24000,
employmentType: "校招",
publishedAt: "2026-08-22 09:20",
collectedAt: "2026-08-23 08:15",
normalizedAt: "2026-08-23 08:19",
quality: 99.3,
status: "有效",
skills: ["数据标准", "元数据", "数据质量", "SQL"],
description: "参与集团数据标准、元数据、主数据和数据质量体系建设。",
contentHash: "9ec1".repeat(16),
payloadHash: "81bd".repeat(16),
dataVersion: "V2026.08.23",
},
{
id: "9dbf174b-4f0d-45ea-aa66-031a42ee802a",
sourceId: "hzrc",
sourceRecordId: "HZRC-20260823-038602",
sourceUrl: "https://www.hzrc.com/job/sample-038602",
taskId: "JOB-20260823-1028",
snapshotId: "RAW-HZRC-20260823-000038",
collectorVersion: "HZRC-Collector-2.7.3",
parserVersion: "PARSER-HZRC-2.7.3",
jobTitleRaw: "低空运营调度师",
jobTitleNormalized: "航空运输服务人员",
jobCode: "AVIATION.LOWALT.DISPATCH",
enterprise: "杭州示例低空运营有限公司",
region: "杭州",
regionCode: "330100",
workplace: "杭州市余杭区",
salaryMin: 9000,
salaryMax: 15000,
employmentType: "全职",
publishedAt: "2026-08-23 07:25",
collectedAt: "2026-08-23 10:28",
normalizedAt: "2026-08-23 10:31",
quality: 92.8,
status: "有效",
skills: ["飞行计划", "运行控制", "空域管理", "应急处置"],
description: "负责低空飞行任务排班、运行监控、空域协调和异常事件处置。",
contentHash: "d09f".repeat(16),
payloadHash: "375a".repeat(16),
dataVersion: "V2026.08.23",
},
{
id: "f45cf9a9-b6e7-4249-bb11-fec640df9dae",
sourceId: "boss",
sourceRecordId: "BOSS-20260823-884733",
sourceUrl: "https://www.bosszhipin.com/job_detail/sample-884733.html",
taskId: "JOB-20260823-1042",
snapshotId: "RAW-BOSS-20260823-000885",
collectorVersion: "BOSS-Collector-4.6.2",
parserVersion: "PARSER-BOSS-4.6.2",
jobTitleRaw: "新能源电池工艺工程师",
jobTitleNormalized: "电池制造工程技术人员",
jobCode: "ENERGY.BATTERY.PROCESS",
enterprise: "宁波示例新能源科技有限公司",
region: "宁波",
regionCode: "330200",
workplace: "宁波市北仑区",
salaryMin: 13000,
salaryMax: 21000,
employmentType: "全职",
publishedAt: "2026-08-23 08:10",
collectedAt: "2026-08-23 10:42",
normalizedAt: "2026-08-23 10:46",
quality: 98.4,
status: "有效",
skills: ["锂电池", "工艺设计", "DOE", "质量分析"],
description: "负责电池制造工艺设计、参数优化、量产导入和制程质量改进。",
contentHash: "bfd7".repeat(16),
payloadHash: "41e2".repeat(16),
dataVersion: "V2026.08.23",
},
{
id: "9286399d-da74-408d-9882-4e04fb84ce6a",
sourceId: "zhaopin",
sourceRecordId: "ZL-20260823-621006",
sourceUrl: "https://www.zhaopin.com/jobdetail/sample-621006.htm",
taskId: "JOB-20260823-1038",
snapshotId: "RAW-ZHAOPIN-20260823-000621",
collectorVersion: "ZHAOPIN-Collector-3.8.1",
parserVersion: "PARSER-ZHAOPIN-3.8.1",
jobTitleRaw: "跨境电商数据分析师",
jobTitleNormalized: "商务数据分析师",
jobCode: "EC.DATA.ANALYST",
enterprise: "嘉兴示例国际贸易有限公司",
region: "嘉兴",
regionCode: "330400",
workplace: "嘉兴市秀洲区",
salaryMin: 8500,
salaryMax: 14000,
employmentType: "全职",
publishedAt: "2026-08-22 17:30",
collectedAt: "2026-08-23 10:38",
normalizedAt: "2026-08-23 10:43",
quality: 98.2,
status: "有效",
skills: ["SQL", "Power BI", "跨境电商", "指标体系"],
description: "负责跨境业务经营分析、指标看板建设、用户分层和专题复盘。",
contentHash: "92bd".repeat(16),
payloadHash: "d371".repeat(16),
dataVersion: "V2026.08.23",
},
{
id: "4c974cea-17fc-450e-8973-17a6be52851a",
sourceId: "brzp",
sourceRecordId: "BRZP-20260823-110309",
sourceUrl: "https://brzp.chinasme.cn/brzp/job/sample-110309",
taskId: "JOB-20260823-0955",
snapshotId: "RAW-BRZP-20260823-000111",
collectorVersion: "BRZP-Collector-2.1.0",
parserVersion: "PARSER-BRZP-2.1.0",
jobTitleRaw: "AI内容运营实习生",
jobTitleNormalized: "互联网营销师",
jobCode: "MEDIA.AI.CONTENT",
enterprise: "苏州示例数字内容有限公司",
region: "苏州",
regionCode: "320500",
workplace: "苏州市工业园区",
salaryMin: 4000,
salaryMax: 6000,
employmentType: "实习",
publishedAt: "2026-08-22 20:10",
collectedAt: "2026-08-23 09:55",
normalizedAt: "2026-08-23 10:02",
quality: 96.9,
status: "有效",
skills: ["AIGC", "内容策划", "短视频", "数据复盘"],
description: "协助生成式内容策划、短视频运营、素材管理和传播效果复盘。",
contentHash: "ac53".repeat(16),
payloadHash: "5b28".repeat(16),
dataVersion: "V2026.08.23",
},
];
const rawSnapshots = [
["RAW-BOSS-20260823-01", "boss", "JOB-20260823-1042", "text/html + JSON", "68,420", "8.42 GB", "2026-08-23 10:42", "4f8d91a47e2b6d82b2aaad136d344d61c83f9cd472185d2543725c91870e9e32", "已固化"],
["RAW-ZHAOPIN-20260823-01", "zhaopin", "JOB-20260823-1038", "text/html + JSON", "52,360", "6.18 GB", "2026-08-23 10:38", "c2d8f733e6711219823110d7d35fdc5535f133ed60f60950f9dc78a228cb402d", "已固化"],
["RAW-BRZP-20260823-01", "brzp", "JOB-20260823-0955", "text/html", "18,640", "1.86 GB", "2026-08-23 09:55", "a831bd0d4e29cab1e650f36a4fc9c5d5cc8053f560b5d3dc7aac62b384b47e11", "已固化"],
["RAW-NCSS-20260823-01", "ncss", "JOB-20260823-0840", "text/html", "14,820", "1.42 GB", "2026-08-23 08:40", "18f4".repeat(16), "已固化"],
["RAW-MOHRSS-20260823-01", "mohrss", "JOB-20260823-0826", "text/html + JSON", "12,680", "1.28 GB", "2026-08-23 08:26", "66a8".repeat(16), "已固化"],
["RAW-IGUOPIN-20260823-01", "iguopin", "JOB-20260823-0815", "text/html", "9,640", "968 MB", "2026-08-23 08:15", "9ec1".repeat(16), "已固化"],
["RAW-HZRC-20260823-02", "hzrc", "JOB-20260823-1028", "text/html", "3,860", "384 MB", "2026-08-23 10:28", "d09f".repeat(16), "已固化"],
].map(([id, sourceId, taskId, mediaType, records, size, capturedAt, sha256, status]) => ({
id,
sourceId,
taskId,
mediaType,
records,
size,
capturedAt,
sha256,
status,
objectPath: `raw/2026/08/23/${sourceId}/${id.toLowerCase()}.jsonl.zst`,
}));
const recordRawSnapshots = standardJobRecords.map((record, index) => ({
id: record.snapshotId,
sourceId: record.sourceId,
taskId: record.taskId,
mediaType: "text/html + JSON",
records: "1",
size: `${84 + index * 7} KB`,
capturedAt: record.collectedAt,
sha256: record.payloadHash,
status: "已固化",
objectPath: `raw/2026/08/23/${record.sourceId}/${record.taskId.toLowerCase()}/${record.snapshotId.toLowerCase()}.response.zst`,
recordId: record.id,
}));
const allRawSnapshots = [...recordRawSnapshots, ...rawSnapshots];
const quarantineRecords = [
["QUA-20260823-001286", "boss", "BOSS-20260823-884890", "智能装备技术支持", "薪资上下限倒置", "SL-004", "2026-08-23 10:43", "待自动重解析", "JOB-20260823-1042", "RAW-BOSS-20260823-000886", "杭州"],
["QUA-20260823-001285", "zhaopin", "ZL-20260823-621104", "工业软件实施顾问", "工作地区代码缺失", "RQ-012", "2026-08-23 10:39", "待字段补全", "JOB-20260823-1038", "RAW-ZHAOPIN-20260823-000622", "宁波"],
["QUA-20260823-001284", "brzp", "BRZP-20260823-110408", "电商运营", "来源详情页已失效", "TM-007", "2026-08-23 09:57", "等待次日复核", "JOB-20260823-0955", "RAW-BRZP-20260823-000112", "嘉兴"],
["QUA-20260823-001283", "hzrc", "HZRC-20260823-038711", "AI内容策略师", "岗位词典未匹配", "NM-021", "2026-08-23 10:30", "进入人工归一", "JOB-20260823-1028", "RAW-HZRC-20260823-000039", "杭州"],
["QUA-20260823-001282", "mohrss", "MOHRSS-20260823-226955", "设备维护员", "企业名称完整性不足", "RQ-018", "2026-08-23 08:28", "待规则复核", "JOB-20260823-0826", "RAW-MOHRSS-20260823-000227", "绍兴"],
].map(([id, sourceId, sourceRecordId, jobTitle, issue, rule, detectedAt, disposition, taskId, snapshotId, region]) => ({
id,
sourceId,
sourceRecordId,
jobTitle,
issue,
rule,
detectedAt,
disposition,
taskId,
snapshotId,
region,
}));
const contracts = {
"external-job": {
key: "JOB-SIGNAL",
title: "外部招聘岗位信号标准",
version: "V3.0.0",
description: "统一商业招聘平台、公共就业平台和活动聚合平台的岗位、企业、薪资、技能与来源证据,用于产教供需分析。",
schemaPath: "./schemas/external-job-source.schema.json",
fields: 39,
required: 19,
owner: "平台数据标准委员会",
status: "生产已发布",
release: "2026-08-18",
compatibility: "向后兼容 V2.6",
fieldsPreview: [
["record_id", "string/uuid", "是", "平台记录唯一标识", "UUID v4"],
["source_platform", "string", "是", "来源平台代码", "受控代码表"],
["source_record_id", "string", "是", "来源记录标识", "同平台唯一"],
["source_url", "string/uri", "是", "来源页面或接口定位", "http/https"],
["collected_at", "datetime", "是", "实际采集时间", "ISO-8601 +08:00"],
["published_at", "datetime", "否", "源端发布时间", "ISO-8601"],
["job_title_raw", "string", "是", "岗位原始名称", "1—200字符"],
["job_title_normalized", "string", "否", "统一岗位名称", "岗位词典 V4.3"],
["enterprise_name", "string", "是", "企业名称", "保留原文"],
["region_code", "string", "是", "工作地区代码", "GB/T 2260"],
["salary_min", "number", "否", "月薪下限(元)", "非负数"],
["salary_max", "number", "否", "月薪上限(元)", "≥ salary_min"],
["skills", "array<string>", "否", "解析后的技能标签", "技能词典 V3.8"],
["content_hash", "string", "是", "原始内容哈希", "SHA-256"],
],
rules: [
["唯一与幂等", "source_platform + source_record_id 唯一;重复采集必须幂等更新。"],
["来源证据", "必须保留 source_url、collected_at、content_hash 和采集任务编号。"],
["薪资口径", "统一折算为税前月薪;面议、日薪、时薪保留原文并标注换算状态。"],
["时效与失效", "岗位状态和最后发现时间分开记录;失效采用版本化软删除。"],
["文本治理", "岗位职责原文进入原始层,分析层只保留去标记、去联系方式后的业务文本。"],
],
},
"school-employment": {
key: "SCHOOL-EMPLOYMENT",
title: "校内就业事实数据标准",
version: "V2.2.0",
description: "统一学校毕业生就业状态、岗位、企业、薪资、专业相关度和去向确认事实,支持租户隔离和敏感字段最小化。",
schemaPath: "./schemas/school-employment.schema.json",
fields: 32,
required: 16,
owner: "平台数据标准委员会",
status: "待生产发布",
release: "计划 2026-08-28",
compatibility: "兼容 V2.1,新增5字段",
fieldsPreview: [
["tenant_id", "string", "是", "学校租户标识", "平台分配"],
["student_token", "string", "是", "学生不可逆令牌", "禁止明文学号"],
["cohort", "integer", "是", "毕业届次", "YYYY"],
["program_code", "string", "是", "专业代码", "教育部专业目录"],
["employment_status", "string", "是", "就业状态", "受控枚举"],
["job_title", "string", "否", "就业岗位", "业务原文"],
["enterprise_token", "string", "否", "企业统一标识", "信用代码令牌化"],
["salary_monthly", "number", "否", "税前月薪", "元/月"],
["job_relevance", "string", "否", "岗位专业相关度", "高/中/低/未确认"],
["confirmed_at", "datetime", "是", "业务确认时间", "ISO-8601"],
["source_system", "string", "是", "校内来源系统", "系统代码"],
["updated_at", "datetime", "是", "业务更新时间", "增量水位"],
],
rules: [
["租户隔离", "所有记录必须携带 tenant_id,存储、任务、查询和导出均按租户强制隔离。"],
["身份最小化", "平台侧使用 student_token,不接收姓名、身份证和明文学号。"],
["业务确认", "就业状态必须具有确认人角色、确认时间和来源系统。"],
["增量更新", "updated_at 作为增量水位,主键冲突时以业务更新时间和版本号判定。"],
["质量阈值", "专业代码合法率100%,必填完整率≥95%,学生专业关联率≥98%。"],
],
},
"package-manifest": {
key: "PACKAGE-MANIFEST",
title: "数据交付包清单标准",
version: "V1.2.0",
description: "定义每次导入、导出和租户交付必须携带的数据版本、文件、Schema、质量、血缘、范围和校验信息。",
schemaPath: "./schemas/package-manifest.schema.json",
fields: 21,
required: 14,
owner: "平台数据交付组",
status: "生产已发布",
release: "2026-08-23",
compatibility: "兼容 V1.1,新增2个发布字段",
fieldsPreview: [
["package_id", "string", "是", "交付包唯一编号", "EDU-PKG-*"],
["release_id", "string", "是", "发布候选编号", "REL-*"],
["release_status", "string", "是", "候选或发布状态", "DRAFT/PUBLISHED"],
["data_product", "string", "是", "数据产品代码", "产品目录"],
["data_version", "string", "是", "业务数据版本", "SemVer/日期版"],
["contract_name", "string", "是", "数据契约名称", "契约注册中心"],
["contract_version", "string", "是", "契约版本", "SemVer"],
["data_as_of", "date", "是", "业务截止日期", "YYYY-MM-DD"],
["tenant_scope", "array<string>", "是", "允许交付租户", "至少1个"],
["record_count", "integer", "是", "有效记录数", "非负整数"],
["files", "array<object>", "是", "数据与附件文件清单", "含SHA-256"],
["quality_score", "number", "是", "综合质量评分", "0—100"],
["lineage_snapshot", "string", "是", "血缘快照编号", "LINEAGE-*"],
["generated_at", "datetime", "是", "生成时间", "ISO-8601"],
],
rules: [
["文件完整性", "Manifest 内每个文件必须记录路径、字节数、媒体类型和 SHA-256。"],
["范围约束", "tenant_scope 与数据服务授权同时校验,不允许客户端自行扩大范围。"],
["契约绑定", "数据文件必须绑定 contract_name 和精确 contract_version。"],
["质量绑定", "必须包含质量报告文件、质量评分和失败规则摘要。"],
["可回滚", "每次交付保留上一稳定版本引用和回滚入口。"],
],
},
};
const exchangeLogs = [
["EXP-20260823-0930", "导出", "区域招聘岗位基础包", "V2026.08.23", "2,846,320", "交付服务", "成功", "09:30"],
["IMP-20260823-0752", "导入", "校内就业36租户增量", "SCH-EMP-20260823", "4,280", "学校接入服务", "成功", "07:52"],
["EXP-20260822-1740", "导出", "薪资与企业画像增强包", "V2026.08.22", "1,286,420", "陈致远", "成功", "昨天 17:40"],
["IMP-20260822-1608", "导入", "企业确认需求批次", "ENT-20260822-03", "8,640", "企业接入服务", "隔离12条", "昨天 16:08"],
];
const qualityRules = [
["UQ", "来源主键唯一", "source_platform + source_record_id", "99.96%", "通过"],
["RQ", "核心字段完整", "岗位、企业、地区、来源、采集时间", "98.84%", "通过"],
["TM", "岗位时效合法", "发布时间、最后发现时间与状态一致", "97.31%", "通过"],
["SL", "薪资区间合法", "币种、周期、上下限和换算状态", "96.72%", "通过"],
["NM", "岗位归一覆盖", "映射到岗位词典 V4.3", "96.40%", "27待审"],
["LN", "来源证据完整", "URL、任务、快照与内容哈希", "100%", "通过"],
];
let normalizationItems = [
["具身智能应用工程师", "机器人应用工程技术人员", "0.91"],
["低空运营调度师", "航空运输服务人员", "0.84"],
["AI内容策略师", "互联网营销师", "0.82"],
["智能驾驶数据闭环工程师", "数据工程技术人员", "0.88"],
["跨境合规运营", "电子商务师", "0.86"],
];
const auditEvents = [
{
id: "AUD-20260823-1042-8821",
time: "10:42:18",
type: "collect",
event: "BOSS增量分片完成",
object: "JOB-20260823-1042 · shard-64",
actor: "collector-boss-prod",
result: "成功",
evidence: ["RAW快照", "响应哈希", "运行日志"],
},
{
id: "AUD-20260823-1038-7412",
time: "10:38:06",
type: "collect",
event: "智联采集水位提交",
object: "SRC-ZL-20260823-04",
actor: "collector-zhaopin-prod",
result: "成功",
evidence: ["水位", "分片清单", "代码版本"],
},
{
id: "AUD-20260823-1028-5630",
time: "10:28:42",
type: "collect",
event: "源端延迟触发降速",
object: "杭州人才网 · P95 4.8s",
actor: "scheduler-protection",
result: "自动保护",
evidence: ["指标快照", "策略版本"],
},
{
id: "AUD-20260823-1016-4482",
time: "10:16:33",
type: "quality",
event: "岗位归一批次完成",
object: "NORM-20260823-06",
actor: "normalization-engine",
result: "27项待审",
evidence: ["输入版本", "词典版本", "模型版本"],
},
{
id: "AUD-20260823-0930-2290",
time: "09:30:12",
type: "publish",
event: "区域岗位基础包发布",
object: "V2026.08.23 · 36租户",
actor: "陈致远",
result: "成功",
evidence: ["审批单", "Manifest", "质量报告"],
},
{
id: "AUD-20260823-0752-1108",
time: "07:52:55",
type: "exchange",
event: "校内就业增量导入",
object: "36租户 · 4,280条",
actor: "school-ingestion-service",
result: "成功",
evidence: ["源版本", "字段映射", "错误明细"],
},
];
const OPERATIONS_STORAGE_KEY = "eduIndustry.vendorOperations.v1";
const OPERATIONS_SCHEMA_VERSION = 2;
const OPERATIONS_ACTOR = "陈致远";
const OPERATIONS_LIMITS = {
auditEvents: 300,
qualityDispositions: 60,
issueTimeline: 120,
exchangeLogs: 100,
};
function operationsSeed() {
const baseEvidence = ["指标快照", "任务日志"];
return {
schemaVersion: OPERATIONS_SCHEMA_VERSION,
seededAt: "2026-08-23T10:30:00+08:00",
issues: [
{
id: "ISSUE-HZRC-20260823-01",
status: "ALERTED",
actor: "scheduler-protection",
time: "2026-08-23T10:28:42+08:00",
trace: "TRACE-HZRC-20260823-5630",
evidence: [...baseEvidence, "P95延迟采样"],
title: "杭州人才网响应时延升高",
severity: "P1",
owner: "待指派",
sla: "2小时内恢复,剩余 01:42",
impact: "杭州都市圈补采降速,8个分片待执行;稳定版本未受影响",
relatedTask: "JOB-20260823-1028",
quarantineId: "QUA-20260823-001283",
sourceId: "hzrc",
qualityRequired: true,
timeline: [
{
id: "STEP-HZRC-ALERT",
status: "ALERTED",
actor: "scheduler-protection",
time: "2026-08-23T10:28:42+08:00",
trace: "TRACE-HZRC-20260823-5630",
evidence: ["指标快照", "降速策略 V2.4"],
note: "P95 4.8s,系统自动将并发从12降至4。",
},
],
},
{
id: "ISSUE-NORM-20260823-01",
status: "ACKNOWLEDGED",
actor: "周敏",
time: "2026-08-23T10:18:00+08:00",
trace: "TRACE-NORM-20260823-4482",
evidence: ["归一批次 NORM-20260823-06", "词典 V4.3"],
title: "27 个岗位别名待人工归一",
severity: "P1",
owner: "周敏",
sla: "今日 16:00 前完成",
impact: "具身智能、低空经济等新增岗位暂未进入稳定分类",
relatedTask: "NORM-20260823-06",
quarantineId: "QUA-20260823-001283",
sourceId: "hzrc",
qualityRequired: false,
timeline: [
{
id: "STEP-NORM-ACK",
status: "ACKNOWLEDGED",
actor: "周敏",
time: "2026-08-23T10:18:00+08:00",
trace: "TRACE-NORM-20260823-4482",
evidence: ["归一待审清单"],
note: "已确认影响范围,等待词典负责人处理。",
},
],
},
{
id: "ISSUE-CONTRACT-20260823-01",
status: "ASSIGNED",
actor: "陈致远",
time: "2026-08-23T09:48:00+08:00",
trace: "TRACE-CONTRACT-20260823-2201",
evidence: ["Schema差异报告", "下游影响清单"],
title: "校内就业契约 V2.2 待发布",
severity: "P2",
owner: "数据标准组",
sla: "2026-08-28 前完成",
impact: "36所学校的新就业质量与去向确认字段尚未启用",
relatedTask: "CONTRACT-SCHOOL-EMPLOYMENT-V2.2",
quarantineId: "无",
sourceId: "school-employment",
qualityRequired: false,
timeline: [
{
id: "STEP-CONTRACT-ASSIGNED",
status: "ASSIGNED",
actor: "陈致远",
time: "2026-08-23T09:48:00+08:00",
trace: "TRACE-CONTRACT-20260823-2201",
evidence: ["责任人指派记录"],
note: "已指派数据标准组执行兼容性与样本回放。",
},
],
},
],
workOrders: [
{
id: "WO-HZRC-20260823-01",
status: "OPEN",
actor: "scheduler-protection",
time: "2026-08-23T10:28:42+08:00",
trace: "TRACE-HZRC-20260823-5630",
evidence: ["自动建单规则"],
issueId: "ISSUE-HZRC-20260823-01",
assignee: "待指派",
},
{
id: "WO-NORM-20260823-01",
status: "ASSIGNED",
actor: "周敏",
time: "2026-08-23T10:18:00+08:00",
trace: "TRACE-NORM-20260823-4482",
evidence: ["归一待审清单"],
issueId: "ISSUE-NORM-20260823-01",
assignee: "周敏",
},
{
id: "WO-CONTRACT-20260823-01",
status: "ASSIGNED",
actor: "陈致远",
time: "2026-08-23T09:48:00+08:00",
trace: "TRACE-CONTRACT-20260823-2201",
evidence: ["Schema差异报告"],
issueId: "ISSUE-CONTRACT-20260823-01",
assignee: "数据标准组",
},
],
qualityDispositions: [],
releases: [
{
id: "REL-20260823-01",
status: "DRAFT",
actor: "陈致远",
time: "2026-08-23T10:30:00+08:00",
trace: "TRACE-REL-20260823-01",
evidence: ["候选数据快照", "质量报告 QUALITY-20260823-09"],
dataVersion: "V2026.08.23",
packageId: "EDU-PKG-20260823-01",
previousStableVersion: "V2026.08.22",
qualityGate: "BLOCKED",
qualityScore: 94.7,
impactTenants: ["TENANT-HZVTC", "TENANT-ZIME", "TENANT-NBCC"],
applicant: "陈致远",
approvalId: "APPROVAL-REL-20260823-01",
manifestGeneratedAt: null,
candidateManifest: null,
candidateInputFingerprint: null,
publishedManifestGeneratedAt: null,
publishedManifest: null,
publishedAt: null,
},
],
approvals: [
{
id: "APPROVAL-REL-20260823-01",
status: "DRAFT",
actor: "陈致远",
time: "2026-08-23T10:30:00+08:00",
trace: "TRACE-APPROVAL-20260823-01",
evidence: ["审批规则 RBAC-RELEASE-2P"],
releaseId: "REL-20260823-01",
applicant: "陈致远",
approver: null,
comment: "",
decidedAt: null,
},
{
id: "APPROVAL-ROLLBACK-REL-20260823-01",
status: "DRAFT",
actor: "陈致远",
time: "2026-08-23T10:30:00+08:00",
trace: "TRACE-ROLLBACK-APPROVAL-20260823-01",
evidence: ["回滚审批规则 RBAC-ROLLBACK-2P"],
releaseId: "REL-20260823-01",
type: "ROLLBACK",
applicant: "陈致远",
approver: null,
comment: "",
reason: "",
decidedAt: null,
},
],
deliveries: [
{
id: "DELIVERY-HZVTC-20260823-01",
status: "PENDING",
actor: "delivery-orchestrator",
time: "2026-08-23T10:30:00+08:00",
trace: "TRACE-DELIVERY-HZVTC-01",
evidence: ["租户范围策略"],
tenantId: "TENANT-HZVTC",
school: "杭州科技职业技术学院",
packageId: "EDU-PKG-20260823-01",
dataVersion: "V2026.08.23",
expectedRecords: 2846320,
receiptRecords: null,
expectedSha: "4f8d91a47e2b6d82b2aaad136d344d61c83f9cd472185d2543725c91870e9e32",
receiptSha: null,
attempts: 0,
failureReason: "",
},
{
id: "DELIVERY-ZIME-20260823-01",
status: "PENDING",
actor: "delivery-orchestrator",
time: "2026-08-23T10:30:00+08:00",
trace: "TRACE-DELIVERY-ZIME-01",
evidence: ["租户范围策略"],
tenantId: "TENANT-ZIME",
school: "浙江机电职业技术大学",
packageId: "EDU-PKG-20260823-01",
dataVersion: "V2026.08.23",
expectedRecords: 2819044,
receiptRecords: null,
expectedSha: "96c2be742dc43f8e7ee526db6358030873406c450f15e67225c605dc39b431a8",
receiptSha: null,
attempts: 0,
failureReason: "",
},
{
id: "DELIVERY-NBCC-20260823-01",
status: "PENDING",
actor: "delivery-orchestrator",
time: "2026-08-23T10:30:00+08:00",
trace: "TRACE-DELIVERY-NBCC-01",
evidence: ["租户范围策略"],
tenantId: "TENANT-NBCC",
school: "宁波城市职业技术学院",
packageId: "EDU-PKG-20260823-01",
dataVersion: "V2026.08.23",
expectedRecords: 2768420,
receiptRecords: null,
expectedSha: "c2d8f733e6711219823110d7d35fdc5535f133ed60f60950f9dc78a228cb402d",
receiptSha: null,
attempts: 0,
failureReason: "",
},
],
auditEvents: [
{
id: "AUD-OPS-20260823-1028",
status: "RECORDED",
actor: "scheduler-protection",
time: "2026-08-23T10:28:42+08:00",
trace: "TRACE-HZRC-20260823-5630",
evidence: ["指标快照", "策略版本"],
type: "operations",
event: "杭州人才网时延告警建单",
object: "ISSUE-HZRC-20260823-01",
result: "待确认",
},
],
};
}
function isOperationsEntityValid(entity) {
return entity && typeof entity.id === "string" && typeof entity.status === "string" && typeof entity.actor === "string" && typeof entity.time === "string" && typeof entity.trace === "string" && Array.isArray(entity.evidence);
}
function trimOperationsStore(store) {
store.auditEvents = store.auditEvents.slice(0, OPERATIONS_LIMITS.auditEvents);
store.qualityDispositions = store.qualityDispositions.slice(-OPERATIONS_LIMITS.qualityDispositions);
store.issues.forEach((issue) => {
issue.timeline = issue.timeline.slice(-OPERATIONS_LIMITS.issueTimeline);
});
return store;
}
function loadOperationsStore() {
try {
const parsed = JSON.parse(localStorage.getItem(OPERATIONS_STORAGE_KEY));
const collections = ["issues", "workOrders", "qualityDispositions", "releases", "approvals", "deliveries", "auditEvents"];
if (
parsed?.schemaVersion === OPERATIONS_SCHEMA_VERSION &&
collections.every((key) => Array.isArray(parsed[key]) && parsed[key].every(isOperationsEntityValid))
) {
return trimOperationsStore(parsed);
}
} catch {
// Invalid or unavailable browser storage falls back to a reproducible seed.
}
const seed = operationsSeed();
try {
localStorage.setItem(OPERATIONS_STORAGE_KEY, JSON.stringify(seed));
} catch {
// The in-memory seed remains fully interactive when storage is blocked.
}
return seed;
}
let operationsStore = loadOperationsStore();
function saveOperationsStore() {
trimOperationsStore(operationsStore);
try {
localStorage.setItem(OPERATIONS_STORAGE_KEY, JSON.stringify(operationsStore));
} catch {
showToast("浏览器阻止了本地存储,本次状态仅在当前页面有效", "状态未持久化");
}
}
function operationsNow() {
return new Date().toISOString();
}
function operationToken(prefix) {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 7).toUpperCase()}`;
}
function addOperationsAudit({ type = "operations", event, object, actor = OPERATIONS_ACTOR, result, evidence = [] }) {
const time = operationsNow();
const trace = operationToken("TRACE");
operationsStore.auditEvents.unshift({
id: operationToken("AUD"),
status: "RECORDED",
actor,
time,
trace,
evidence: [...evidence],
type,
event,
object,
result,
});
return { actor, time, trace, evidence: [...evidence] };
}
function migrateOperationsEntity(entity, status, metadata) {
entity.status = status;
entity.actor = metadata.actor;
entity.time = metadata.time;
entity.trace = metadata.trace;
entity.evidence = [...metadata.evidence];
}
const state = {
sourceFilter: "all",
sourceSearch: "",
sourceId: "boss",
sourceTab: "overview",
jobFilter: "all",
recordLayer: "standard",
recordSource: "all",
recordRegion: "all",
recordSearch: "",
recordId: "",
contractKey: "external-job",
auditFilter: "all",
operationsTab: "issues",
issueId: "",
};
function h(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function showToast(message, title = "操作已完成") {
const toast = document.createElement("div");
toast.className = "toast";
toast.innerHTML = `<span>✓</span><span><strong>${h(title)}</strong><small>${h(message)}</small></span>`;
document.getElementById("toastRegion").appendChild(toast);
setTimeout(() => toast.remove(), 3600);
}
function openModal(modal) {
modal.classList.add("open");
modal.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeModal(modal) {
modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function switchView(viewName) {
document.querySelectorAll(".view").forEach((view) => view.classList.toggle("active", view.id === `view-${viewName}`));
document.querySelectorAll(".nav-item[data-view]").forEach((button) => button.classList.toggle("active", button.dataset.view === viewName));
const activeView = document.getElementById(`view-${viewName}`);
document.getElementById("breadcrumbCurrent").textContent = activeView?.dataset.title || "运行总览";
document.body.classList.remove("sidebar-open");
window.scrollTo({ top: 0, behavior: "smooth" });
}
function formatNumber(value) {
return Number(value).toLocaleString("zh-CN");
}
function sourceById(id) {
return sources.find((source) => source.id === id) || sources[0];
}
function sourceStatusPill(source) {
const tone = source.status === "attention" ? "attention" : source.status === "failed" ? "failed" : "running";
return `<span class="status-pill ${tone}">${h(source.statusLabel)}</span>`;
}
const operationsStatusLabels = {
ALERTED: "待确认",
ACKNOWLEDGED: "已确认",
ASSIGNED: "已指派",
INVESTIGATING: "排障中",
QUALITY_REVIEW_PENDING: "待质量复核",
VERIFIED: "复核通过",
REVIEWED_BLOCKED: "复核后仍阻断",
RESOLVED: "已解决",
CLOSED: "已关闭",
OPEN: "已建单",
DRAFT: "草稿",
PENDING_APPROVAL: "待审批",
APPROVED: "已批准",
REJECTED: "已驳回",
PUBLISHED: "已发布",
ROLLED_BACK: "已回滚",
PENDING: "待发送",
SENDING: "发送中",
DELIVERED: "已送达",
ACKNOWLEDGED_DELIVERY: "已确认回执",
ACKNOWLEDGED: "已确认",
FAILED: "失败",
RETRYING: "重试中",
PENDING_REVIEW: "待复核",
RECORDED: "已记录",
PASS: "通过",
BLOCKED: "阻断",
ROLLBACK_PENDING_APPROVAL: "回滚待审批",
ROLLBACK_APPROVED: "回滚已批准",
ROLLBACK_REJECTED: "回滚已驳回",
ROLLBACK_EXECUTED: "回滚已执行",
};
function operationStatusLabel(status) {
return operationsStatusLabels[status] || status;
}
function operationStatusTone(status) {
if (["FAILED", "REJECTED", "BLOCKED", "REVIEWED_BLOCKED", "ROLLBACK_REJECTED"].includes(status)) return "failed";
if (["ALERTED", "PENDING_APPROVAL", "QUALITY_REVIEW_PENDING", "PENDING", "RETRYING", "ASSIGNED", "ROLLBACK_PENDING_APPROVAL"].includes(status)) return "attention";
if (
[
"VERIFIED",
"RESOLVED",
"CLOSED",
"APPROVED",
"PUBLISHED",
"DELIVERED",
"ACKNOWLEDGED",
"ROLLED_BACK",
"PASS",
"ROLLBACK_APPROVED",
"ROLLBACK_EXECUTED",
].includes(status)
)
return "running";
return "";
}
function operationTime(value) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString("zh-CN", { hour12: false });
}
function currentRelease() {
return operationsStore.releases.find((release) => release.id === "REL-20260823-01");
}
function currentApproval() {
const release = currentRelease();
return operationsStore.approvals.find((approval) => approval.id === release?.approvalId);
}
function currentRollbackApproval() {
const release = currentRelease();
return operationsStore.approvals.find((approval) => approval.releaseId === release?.id && approval.type === "ROLLBACK");
}
function isIndependentApproval(approval, approver = approval?.approver) {
return Boolean(approval && approver && approver !== approval.applicant);
}
function decideIndependentApproval({
approval,
target,
approver,
comment,
approved,
approveStatus,
rejectStatus,
approveEvent,
rejectEvent,
object,
evidence,
defaultApproveComment,
defaultRejectComment,
resultLabel = (status) => status,
successMessage,
successTitle,
}) {
if (!isIndependentApproval(approval, approver)) {
const rollback = approval?.type === "ROLLBACK";
showToast(rollback ? "回滚申请人与审批人相同,四眼规则阻止了该操作" : "申请人和审批人相同,四眼审批规则阻止了该操作", rollback ? "无法批准自己的回滚" : "无法批准自己的申请");
return false;
}
const nextStatus = approved ? approveStatus : rejectStatus;
const metadata = addOperationsAudit({
type: "publish",
event: approved ? approveEvent : rejectEvent,
object,
actor: approver,
result: resultLabel(nextStatus),
evidence,
});
approval.approver = approver;
approval.comment = comment || (approved ? defaultApproveComment : defaultRejectComment);
approval.decidedAt = metadata.time;
migrateOperationsEntity(approval, nextStatus, metadata);
if (target) migrateOperationsEntity(target, nextStatus, metadata);
saveOperationsStore();
renderOperations();
renderAudit();
showToast(successMessage(approver, approved), successTitle(approved));
return true;
}
function latestQualityDisposition(issueId = "ISSUE-HZRC-20260823-01") {
return operationsStore.qualityDispositions.filter((disposition) => disposition.issueId === issueId).at(-1);
}
function isVerifiedFixReplay(disposition) {
return Boolean(
disposition?.action === "FIX_REPLAY" &&
disposition.status === "VERIFIED" &&
disposition.verification?.replayedRecords === disposition.verification?.expectedRecords &&
disposition.verification?.rulesPassed === true &&
disposition.verification?.reviewerSigned === true,
);
}
function qualityGatePassed() {
return isVerifiedFixReplay(latestQualityDisposition());
}
function updateReleaseQualityGate() {
const release = currentRelease();
if (!release || ["PUBLISHED", "ROLLED_BACK"].includes(release.status)) return;
release.qualityGate = qualityGatePassed() ? "PASS" : "BLOCKED";
}
function releaseReadyForApproval(release = currentRelease()) {
return Boolean(release?.qualityGate === "PASS" && release.manifestGeneratedAt && release.candidateManifest);
}
function renderOperationsMetrics() {
const openIssues = operationsStore.issues.filter((issue) => !["RESOLVED", "CLOSED"].includes(issue.status)).length;
const pendingApprovals = operationsStore.approvals.filter((approval) => ["PENDING_APPROVAL", "ROLLBACK_PENDING_APPROVAL"].includes(approval.status)).length;
const deliveryAttention = operationsStore.deliveries.filter((delivery) => ["PENDING", "SENDING", "FAILED", "RETRYING"].includes(delivery.status)).length;
const acknowledged = operationsStore.deliveries.filter((delivery) => delivery.status === "ACKNOWLEDGED").length;
document.getElementById("operationsMetrics").innerHTML = `
<article><span>未闭环问题</span><strong>${openIssues}</strong><small>确认、指派、排障与质量处置</small></article>
<article><span>审批待办</span><strong>${pendingApprovals}</strong><small>申请人与审批人强制分离</small></article>
<article><span>交付待办 / 异常</span><strong>${deliveryAttention}</strong><small>按租户隔离,不重复发送成功学校</small></article>
<article><span>已核验回执</span><strong>${acknowledged}</strong><small>记录数与 SHA-256 均一致</small></article>
`;
document.getElementById("operationsNavCount").textContent = openIssues + pendingApprovals + deliveryAttention;
document.getElementById("notificationButton").querySelector("b").textContent = openIssues + pendingApprovals + deliveryAttention;
const release = currentRelease();
const releaseReady = releaseReadyForApproval(release);
const overviewState = document.getElementById("overviewReleaseState");
const overviewDetail = document.getElementById("overviewReleaseDetail");
const qualityState = document.getElementById("qualityReleaseState");
const qualityDetail = document.getElementById("qualityReleaseDetail");
if (overviewState) overviewState.textContent = release?.status === "PUBLISHED" ? "稳定版本已发布" : releaseReady ? "候选可提交审批" : "候选发布受阻";
if (overviewDetail) overviewDetail.textContent = release?.qualityGate === "PASS" ? (release?.manifestGeneratedAt ? "质量与候选 Manifest 已就绪" : "质量通过,待生成候选 Manifest") : "杭州人才网修复重放尚未验证";
if (qualityState) qualityState.textContent = release?.qualityGate === "PASS" ? "当前批次质量门禁已通过" : "当前批次仍需修复重放复核";
if (qualityDetail) qualityDetail.textContent = release?.qualityGate === "PASS" ? "质量证据已通过独立复核,可继续生成候选包" : "质量分达标不等于门禁通过,稳定版本继续保持不变";
}
function renderOverviewIssues() {
const issues = operationsStore.issues.filter((issue) => !["RESOLVED", "CLOSED"].includes(issue.status)).slice(0, 3);
const list = document.getElementById("overviewIssueList");
list.innerHTML = issues.length
? issues
.map(
(issue) => `
<button data-open-issue="${h(issue.id)}">
<i class="${issue.severity === "P1" ? "red" : issue.severity === "P2" ? "amber" : "blue"}"></i>
<span><strong>${h(issue.title)}</strong><small>${h(issue.impact)}</small></span>
<em>${h(issue.severity)}</em>
</button>
`,
)
.join("")
: `<div class="info-note">当前没有待处理问题,所有事项均已闭环。</div>`;
const countBadge = list.closest(".alert-panel").querySelector(".panel-head > em");
countBadge.textContent = `${issues.length} 项`;
}
function renderOperationsIssues() {
const rows = operationsStore.issues
.map((issue) => {
const last = issue.timeline.at(-1);
return `
<tr>
<td><span class="job-title"><strong>${h(issue.title)}</strong><small>${h(issue.id)} · ${h(issue.trace)}</small></span></td>
<td><span class="severity-badge ${h(issue.severity.toLowerCase())}">${h(issue.severity)}</span><small class="cell-note">${h(issue.sla)}</small></td>
<td><span class="operations-impact">${h(issue.impact)}</span></td>
<td>${h(issue.owner)}</td>
<td><span class="status-pill ${operationStatusTone(issue.status)}">${h(operationStatusLabel(issue.status))}</span></td>
<td><span class="record-time-copy"><strong>${h(last?.actor || issue.actor)}</strong><small>${h(operationTime(last?.time || issue.time))}</small></span></td>
<td><div class="row-actions"><button data-open-issue="${h(issue.id)}">进入处置</button></div></td>
</tr>
`;
})
.join("");
document.getElementById("operationsIssueTable").innerHTML = rows;
const openCount = operationsStore.issues.filter((issue) => !["RESOLVED", "CLOSED"].includes(issue.status)).length;
document.getElementById("issueQueueSummary").textContent = `${openCount} 项未闭环 · 状态刷新后保留`;
}
function releaseNextAction(release, approval) {
if (release.status === "DRAFT") {
const ready = releaseReadyForApproval(release);
return `<button class="button primary" data-release-submit ${ready ? "" : "disabled"}>${release.manifestGeneratedAt ? "提交独立审批" : "请先生成候选 Manifest"}</button>`;
}
if (release.status === "PENDING_APPROVAL") {
return `
<label class="approval-field"><span>独立审批人</span><select id="releaseApprover"><option value="林若溪">林若溪 · 数据产品负责人</option><option value="王海宁">王海宁 · 发布审批人</option><option value="陈致远">陈致远 · 当前申请人</option></select></label>
<label class="approval-field approval-comment"><span>审批意见</span><input id="approvalComment" value="质量门禁与租户范围已复核" /></label>
<button class="button secondary" data-release-reject>驳回</button>
<button class="button primary" data-release-approve>批准</button>
`;
}
if (release.status === "REJECTED") {
return `<button class="button primary" data-release-resubmit>返回草稿并重建候选</button>`;
}
if (release.status === "APPROVED") {
return `<button class="button primary" data-release-publish>发布稳定版本</button>`;
}
if (release.status === "PUBLISHED") {
const rollbackApproval = currentRollbackApproval();
if (rollbackApproval?.status === "ROLLBACK_PENDING_APPROVAL") {
return `
<article class="rollback-approval-block">
<strong>全局回滚申请待独立审批</strong>
<small>${h(rollbackApproval.reason)} · 申请人 ${h(rollbackApproval.applicant)} 不能自批</small>
<label class="approval-field"><span>独立审批人</span><select id="rollbackApprover"><option value="林若溪">林若溪 · 数据产品负责人</option><option value="王海宁">王海宁 · 发布审批人</option><option value="陈致远">陈致远 · 当前申请人</option></select></label>
<label class="approval-field approval-comment"><span>审批意见</span><input id="rollbackApprovalComment" value="已核对影响租户、恢复目标与回滚窗口" /></label>
<span class="rollback-action-row"><button class="button secondary" data-rollback-reject>驳回</button><button class="button primary" data-rollback-approve>批准回滚</button></span>
</article>`;
}
if (rollbackApproval?.status === "ROLLBACK_APPROVED") {
return `<button class="button secondary danger-button" data-release-rollback>执行已批准回滚至 ${h(release.previousStableVersion)}</button>`;
}
return `
<label class="approval-field approval-comment"><span>全局回滚原因</span><input id="rollbackReason" value="发布后质量监控异常,需要恢复上一稳定版本" /></label>
<button class="button secondary danger-button" data-rollback-submit>提交独立回滚审批</button>`;
}
return `<span class="release-final-note">当前候选版本已回滚,交付记录仍按租户保留审计。</span>`;
}
function renderReleaseWorkspace() {
updateReleaseQualityGate();
const release = currentRelease();
const approval = currentApproval();
const rollbackApproval = currentRollbackApproval();
document.getElementById("releaseWorkspace").innerHTML = `
<div class="release-workspace-grid">
<article class="panel candidate-card">
<div class="panel-head"><div><span>RELEASE CANDIDATE</span><h2>${h(release.id)}</h2></div><span class="status-pill ${operationStatusTone(release.status)}">${h(operationStatusLabel(release.status))}</span></div>
<div class="candidate-version-banner">
<span><small>数据版本</small><strong>${h(release.dataVersion)}</strong></span>
<span><small>候选包</small><strong>${h(release.packageId)}</strong></span>
<span><small>上一稳定版本</small><strong>${h(release.previousStableVersion)}</strong></span>
</div>
<div class="release-facts">
<span><small>质量门禁</small><strong class="${release.qualityGate === "PASS" ? "gate-pass" : "gate-blocked"}">${h(operationStatusLabel(release.qualityGate))} · ${release.qualityScore}</strong></span>
<span><small>影响租户</small><strong>${release.impactTenants.length} 所学校</strong></span>
<span><small>申请人</small><strong>${h(release.applicant)}</strong></span>
<span><small>Manifest</small><strong>${release.publishedManifestGeneratedAt ? `发布版已生成 · ${h(operationTime(release.publishedManifestGeneratedAt))}` : release.manifestGeneratedAt ? `候选已生成 · ${h(operationTime(release.manifestGeneratedAt))}` : "尚未生成"}</strong></span>
</div>
<div class="release-quality-note ${release.qualityGate === "PASS" ? "passed" : ""}">
${
release.status === "PUBLISHED"
? "质量证据、候选 Manifest 与独立审批已冻结,正式发布 Manifest 已生成;当前尚未代表学校接收。"
: release.status === "ROLLED_BACK"
? `稳定别名已回滚到 ${h(release.previousStableVersion)},原发布、审批和租户交付记录继续保留审计。`
: release.status === "PENDING_APPROVAL"
? "候选 Manifest 已冻结,正在等待不同于申请人的独立审批人处理。"
: release.status === "APPROVED"
? "独立审批已通过,可以执行稳定别名切换并生成正式发布 Manifest。"
: release.status === "REJECTED"
? "独立审批已驳回,旧候选不可继续发布;返回草稿后必须修订并重新生成 Manifest。"
: release.qualityGate === "PASS"
? release.manifestGeneratedAt
? "修复重放证据与候选 Manifest 已就绪,可以提交独立发布审批。"
: "修复重放已由独立复核人验证;还必须生成候选 Manifest 后才能提交审批。"
: "质量门禁阻断:只有 FIX_REPLAY 的记录数、规则结果与复核签名全部验证通过才能解锁;豁免或丢弃均保持阻断。"
}
</div>
<div class="candidate-actions">
${
release.status === "PUBLISHED" && release.publishedManifest
? `<button class="button secondary" data-release-official-manifest>下载发布 Manifest</button>`
: release.status === "DRAFT"
? `<button class="button secondary" data-release-manifest ${release.qualityGate === "PASS" ? "" : "disabled"}>${release.qualityGate === "PASS" ? (release.manifestGeneratedAt ? "重新生成候选 Manifest" : "生成候选包 / Manifest") : "质量通过后生成 Manifest"}</button>`
: `<span class="release-final-note">候选 Manifest 已冻结,审批中不可重建。</span>`
}
<button class="text-button" data-open-issue="ISSUE-HZRC-20260823-01">查看阻断问题</button>
</div>
</article>
<article class="panel approval-card">
<div class="panel-head"><div><span>FOUR-EYES APPROVAL</span><h2>独立审批</h2></div><span class="status-pill ${operationStatusTone(approval.status)}">${h(operationStatusLabel(approval.status))}</span></div>
<dl class="approval-summary">
<div><dt>审批编号</dt><dd>${h(approval.id)}</dd></div>
<div><dt>申请人</dt><dd>${h(approval.applicant)}</dd></div>
<div><dt>审批人</dt><dd>${h(approval.approver || "待独立审批人处理")}</dd></div>
<div><dt>审批意见</dt><dd>${h(approval.comment || "—")}</dd></div>
<div><dt>审批引用</dt><dd>${h(`${approval.id}:${approval.status}`)}</dd></div>
${
rollbackApproval?.status !== "DRAFT"
? `<div><dt>回滚审批</dt><dd>${h(`${rollbackApproval.id}:${rollbackApproval.status}`)} · ${h(rollbackApproval.approver || "待独立审批")}</dd></div>`
: ""
}
</dl>
<div class="approval-actions">${releaseNextAction(release, approval)}</div>
</article>
</div>
`;
}
function deliveryActionButtons(delivery) {
if (delivery.status === "PENDING") return `<span class="cell-note">发布后可启动</span>`;
if (["SENDING", "RETRYING"].includes(delivery.status)) {
return `<div class="row-actions"><button data-delivery-delivered="${h(delivery.id)}">模拟送达</button><button class="secondary" data-delivery-failed="${h(delivery.id)}">模拟失败</button></div>`;
}
if (delivery.status === "FAILED") return `<div class="row-actions"><button data-delivery-retry="${h(delivery.id)}">单校重试</button></div>`;
if (delivery.status === "DELIVERED") {
return `<div class="row-actions"><button data-delivery-ack="${h(delivery.id)}">校验并确认回执</button><button class="secondary" data-delivery-rollback="${h(delivery.id)}">单校回滚</button></div>`;
}
if (delivery.status === "ACKNOWLEDGED") return `<div class="row-actions"><button class="secondary" data-delivery-rollback="${h(delivery.id)}">单校回滚</button></div>`;
return `<span class="cell-note">已完成租户级回滚</span>`;
}
function renderDeliveries() {
const published = currentRelease()?.status === "PUBLISHED";
const button = document.getElementById("startPendingDeliveries");
button.disabled = !published || !operationsStore.deliveries.some((delivery) => delivery.status === "PENDING");
button.textContent = published ? "启动待发送学校" : "候选发布后可启动";
document.getElementById("deliveryTable").innerHTML = operationsStore.deliveries
.map(
(delivery) => `
<tr>
<td><span class="job-title"><strong>${h(delivery.school)}</strong><small>${h(delivery.tenantId)}</small></span></td>
<td><span class="record-time-copy"><strong>${h(delivery.packageId)}</strong><small>${h(delivery.dataVersion)}</small></span></td>
<td><span class="status-pill ${operationStatusTone(delivery.status)}">${h(operationStatusLabel(delivery.status))}</span>${delivery.failureReason ? `<small class="failure-note">${h(delivery.failureReason)}</small>` : ""}</td>
<td>
${
delivery.status === "DELIVERED"
? `<input class="receipt-input" id="receipt-count-${h(delivery.id)}" inputmode="numeric" value="${delivery.receiptRecords ?? delivery.expectedRecords}" aria-label="${h(delivery.school)}回执记录数" />`
: `<span class="record-time-copy"><strong>${delivery.receiptRecords === null ? "—" : formatNumber(delivery.receiptRecords)}</strong><small>期望 ${formatNumber(delivery.expectedRecords)}</small></span>`
}
</td>
<td>
${
delivery.status === "DELIVERED"
? `<input class="receipt-input hash-input" id="receipt-sha-${h(delivery.id)}" value="${h(delivery.receiptSha || delivery.expectedSha)}" aria-label="${h(delivery.school)}回执SHA-256" />`
: `<span class="hash-copy">${h(delivery.receiptSha || "—")}</span>`
}
</td>
<td><span class="record-time-copy"><strong>${h(delivery.actor)}</strong><small>${h(operationTime(delivery.time))} · ${delivery.attempts} 次</small></span></td>
<td>${deliveryActionButtons(delivery)}</td>
</tr>
`,
)
.join("");
}
function renderOperations() {
renderOperationsMetrics();
renderOverviewIssues();
renderOperationsIssues();
renderReleaseWorkspace();
renderDeliveries();
document.querySelectorAll("[data-operations-tab]").forEach((button) => button.classList.toggle("active", button.dataset.operationsTab === state.operationsTab));
document.querySelectorAll(".operations-workspace").forEach((workspace) => workspace.classList.toggle("active", workspace.id === `operations-${state.operationsTab}`));
}
function issueActionFooter(issue) {
const disposition = latestQualityDisposition(issue.id);
const qualityReviewAction =
disposition?.action === "FIX_REPLAY"
? `<button class="button primary" data-issue-action="verify">验证重放并通过</button>`
: `<button class="button primary" data-issue-action="verify-blocked">完成复核并保持阻断</button>`;
const actions = {
ALERTED: `<button class="button primary" data-issue-action="acknowledge">确认告警</button>`,
ACKNOWLEDGED: `<label class="footer-field"><span>指派负责人</span><select id="issueAssignee"><option>周敏 · 区域源运维</option><option>赵亦辰 · 采集工程师</option><option>数据标准组</option></select></label><button class="button primary" data-issue-action="assign">确认指派</button>`,
ASSIGNED: `<button class="button primary" data-issue-action="investigate">开始排障</button>`,
INVESTIGATING: issue.qualityRequired
? `<span class="footer-guidance">填写质量处置并提交复核</span>`
: `<button class="button primary" data-issue-action="resolve">解决问题</button>`,
QUALITY_REVIEW_PENDING: qualityReviewAction,
VERIFIED: `<button class="button primary" data-issue-action="resolve">标记已解决</button>`,
RESOLVED: `<button class="button primary" data-issue-action="close">关闭问题</button>`,
CLOSED: `<span class="release-final-note">问题已闭环,所有动作与证据已保留。</span>`,
};
return `<button class="button secondary" data-issue-close>关闭抽屉</button>${actions[issue.status] || ""}`;
}
function openIssueDrawer(issueId) {
const issue = operationsStore.issues.find((item) => item.id === issueId);
if (!issue) return;
state.issueId = issueId;
const disposition = operationsStore.qualityDispositions.filter((item) => item.issueId === issue.id).at(-1);
document.getElementById("issueDrawerKicker").textContent = `${issue.severity} · ${issue.id}`;
document.getElementById("issueDrawerTitle").textContent = issue.title;
document.getElementById("issueDrawerDescription").textContent = `${operationStatusLabel(issue.status)} · ${issue.owner} · ${issue.sla}`;
document.getElementById("issueDrawerContent").innerHTML = `
<div class="issue-summary-grid">
<span><small>负责人</small><strong>${h(issue.owner)}</strong></span>
<span><small>SLA</small><strong>${h(issue.sla)}</strong></span>
<span><small>关联任务</small><strong>${h(issue.relatedTask)}</strong></span>
<span><small>隔离记录</small><strong>${h(issue.quarantineId)}</strong></span>
</div>
<article class="drawer-card">
<h3>影响与证据</h3>
<p>${h(issue.impact)}</p>
<div class="evidence-tags">${issue.evidence.map((item) => `<span>${h(item)}</span>`).join("")}</div>
</article>
${
issue.status === "INVESTIGATING" && issue.qualityRequired
? `
<article class="drawer-card quality-disposition-form">
<h3>提交质量处置复核</h3>
<div class="form-grid">
<label><span>处置方式</span><select id="qualityDispositionAction"><option value="FIX_REPLAY">修复并重放</option><option value="WAIVE">豁免</option><option value="DISCARD">丢弃</option></select></label>
<label><span>独立复核人</span><select id="qualityReviewer"><option>林若溪</option><option>王海宁</option><option>数据质量组</option></select></label>
<label class="full"><span>处置原因</span><textarea id="qualityDispositionReason" rows="3" placeholder="说明根因、修复范围和重放结果"></textarea></label>
</div>
<button class="button primary wide" data-issue-action="submit-quality">提交质量复核</button>
</article>
`
: ""
}
${
disposition
? `
<article class="drawer-card immutable-action">
<h3>不可变质量动作</h3>
<dl>
<div><dt>动作编号</dt><dd>${h(disposition.id)}</dd></div>
<div><dt>处置方式</dt><dd>${h({ FIX_REPLAY: "修复并重放", WAIVE: "豁免", DISCARD: "丢弃" }[disposition.action])}</dd></div>
<div><dt>原因</dt><dd>${h(disposition.reason)}</dd></div>
<div><dt>复核人 / 状态</dt><dd>${h(disposition.reviewer)} · ${h(operationStatusLabel(disposition.status))}</dd></div>
<div><dt>追踪 / 证据</dt><dd>${h(disposition.trace)} · ${h(disposition.evidence.join("、"))}</dd></div>
</dl>
</article>
`
: ""
}
<article class="drawer-card">
<h3>处置时间线</h3>
<div class="issue-timeline">
${issue.timeline
.map(
(step) => `
<span><i></i><span><strong>${h(operationStatusLabel(step.status))} · ${h(step.actor)}</strong><small>${h(operationTime(step.time))} · ${h(step.note)}</small><em>${h(step.trace)} · ${h(step.evidence.join("、"))}</em></span></span>
`,
)
.join("")}
</div>
</article>
`;
document.getElementById("issueDrawerFooter").innerHTML = issueActionFooter(issue);
const drawer = document.getElementById("issueDrawer");
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeIssueDrawer() {
const drawer = document.getElementById("issueDrawer");
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function transitionIssue(issue, status, note, evidence, actor = OPERATIONS_ACTOR) {
const metadata = addOperationsAudit({
event: `${issue.title}:${operationStatusLabel(status)}`,
object: issue.id,
actor,
result: operationStatusLabel(status),
evidence,
});
migrateOperationsEntity(issue, status, metadata);
issue.timeline.push({
id: operationToken("STEP"),
status,
actor: metadata.actor,
time: metadata.time,
trace: metadata.trace,
evidence: [...metadata.evidence],
note,
});
const workOrder = operationsStore.workOrders.find((item) => item.issueId === issue.id);
if (workOrder) {
migrateOperationsEntity(workOrder, status === "CLOSED" ? "CLOSED" : status, metadata);
workOrder.assignee = issue.owner;
}
saveOperationsStore();
renderOperations();
renderAudit();
openIssueDrawer(issue.id);
}
function renderOverviewSources() {
const selected = sources.slice(0, 5);
document.getElementById("overviewSourceHealth").innerHTML = selected
.map(
(source) => `
<button class="source-health-item" data-source-detail="${h(source.id)}">
<span class="source-health-logo ${h(source.category)}">${h(source.short.slice(0, 2))}</span>
<span class="source-health-copy"><strong>${h(source.name)}</strong><small>${h(source.adapter)} · 更新 ${h(source.updated)}</small></span>
<span class="health-meter"><i><b style="width:${source.health}%"></b></i><strong>${source.health.toFixed(1)}</strong></span>
${sourceStatusPill(source)}
</button>
`,
)
.join("");
}
function filteredSources() {
const keyword = state.sourceSearch.trim().toLowerCase();
return sources.filter((source) => {
const matchesFilter = state.sourceFilter === "all" || source.category === state.sourceFilter;
const text = `${source.name}${source.domain}${source.adapter}${source.categoryLabel}`.toLowerCase();
return matchesFilter && text.includes(keyword);
});
}
function renderSources() {
const filtered = filteredSources();
document.getElementById("sourceGrid").innerHTML = filtered
.map(
(source) => `
<article class="source-card">
<div class="source-card-head">
<span class="source-logo ${h(source.category)}">${h(source.short.slice(0, 2))}</span>
<span class="source-name"><strong>${h(source.name)}</strong><small>${h(source.domain)}</small></span>
<em class="source-type ${h(source.category)}">${h(source.categoryLabel)}</em>
</div>
<div class="source-card-metrics">
<span><small>健康度</small><strong>${source.health.toFixed(1)}</strong></span>
<span><small>今日新增</small><strong>${formatNumber(source.today)}</strong></span>
<span><small>累计有效</small><strong>${h(source.total)}</strong></span>
</div>
<div class="source-card-meta">
<span>${h(source.mode)}</span><span>${h(source.schedule)}</span><span>${h(source.contract)}</span>
</div>
<div class="source-card-foot">
<span>${h(source.statusLabel)} · 更新于 ${h(source.updated)}</span>
<div class="source-card-actions">
${source.category === "school" ? "" : `<button data-source-records="${h(source.id)}">查看数据</button>`}
<button data-source-detail="${h(source.id)}">进入工作台</button>
</div>
</div>
</article>
`,
)
.join("");
if (!filtered.length) {
document.getElementById("sourceGrid").innerHTML = `<div class="info-note">没有找到符合条件的数据源,请调整分类或搜索关键词。</div>`;
}
document.getElementById("sourceListSummary").textContent = `当前展示 ${filtered.length} 个数据源`;
document.getElementById("sourceTotal").textContent = sources.length;
document.getElementById("sourceRunning").textContent = sources.filter((source) => source.status === "running").length;
document.getElementById("sourceNavCount").textContent = sources.length;
}
function drawerStats(source) {
return `
<div class="drawer-stat-grid">
<span><small>当前源版本</small><strong>${h(source.version)}</strong><em>${h(source.updated)} 更新</em></span>
<span><small>综合健康度</small><strong>${source.health.toFixed(1)}</strong><em>${h(source.statusLabel)}</em></span>
<span><small>今日有效记录</small><strong>${formatNumber(source.today)}</strong><em>累计 ${h(source.total)}</em></span>
<span><small>目标契约</small><strong>${h(source.contract)}</strong><em>${h(source.schema)}</em></span>
</div>
`;
}
function renderSourceOverview(source) {
return `
${drawerStats(source)}
<div class="drawer-contract-banner">
<span>${h(source.categoryLabel.toUpperCase())} · ${h(source.adapter)}</span>
<strong>${h(source.coverage)}</strong>
<small>${h(source.change)}</small>
</div>
<article class="drawer-card">
<h3>资产责任与运行策略</h3>
<dl>
<div><dt>来源入口</dt><dd>${h(source.domain)}</dd></div>
<div><dt>接入方式</dt><dd>${h(source.mode)}</dd></div>
<div><dt>更新频率</dt><dd>${h(source.schedule)}</dd></div>
<div><dt>技术责任人</dt><dd>${h(source.owner)}</dd></div>
<div><dt>原始数据留存</dt><dd>${h(source.retention)}</dd></div>
<div><dt>连接器资产</dt><dd>${h(source.repo)} · ${h(source.codeVersion)}</dd></div>
</dl>
</article>
<article class="drawer-card">
<h3>进入生产的控制点</h3>
<ul>
<li>先保存原始响应与内容哈希,再执行解析、清洗和岗位归一。</li>
<li>解析规则、代码版本、任务分片和增量水位与数据版本绑定。</li>
<li>新采集批次未通过质量验收前,不覆盖当前稳定交付版本。</li>
</ul>
</article>
`;
}
function renderSourceAcquisition(source) {
return `
${drawerStats(source)}
<article class="drawer-card">
<h3>生产采集配置</h3>
<dl>
<div><dt>采集模式</dt><dd>${h(source.mode)}</dd></div>
<div><dt>调度计划</dt><dd>${h(source.schedule)}</dd></div>
<div><dt>并发与速率</dt><dd>${h(source.rate)}</dd></div>
<div><dt>失败重试</dt><dd>${h(source.retry)}</dd></div>
<div><dt>采集范围</dt><dd>${h(source.coverage)}</dd></div>
<div><dt>目标数据契约</dt><dd>${h(source.contract)}</dd></div>
</dl>
</article>
<article class="drawer-card">
<h3>任务执行要求</h3>
<ul>
<li>所有请求必须携带 task_id、shard_id、collector_version 和 trace_id。</li>
<li>源端异常触发自动降速、熔断或分片隔离,不影响其他来源继续运行。</li>
<li>增量任务必须提交已验收水位;失败重试保持幂等。</li>
<li>动态页面、接口响应和文件下载均进入原始留存层,解析过程可重放。</li>
</ul>
</article>
`;
}
function renderSourceMapping(source) {
const rows =
source.category === "school"
? [
["student_key", "student_token", "令牌化", "必填"],
["major_code", "program_code", "专业代码字典", "必填"],
["job_name", "job_title", "去空白与标准化", "条件必填"],
["company_code", "enterprise_token", "企业标识令牌化", "条件必填"],
["salary", "salary_monthly", "统一元/月", "可选"],
["update_time", "updated_at", "ISO-8601", "必填"],
]
: [
["source_id", "source_record_id", "原样保留", "必填"],
["job_name", "job_title_raw", "去标记与空白", "必填"],
["company_name", "enterprise_name", "原文 + 企业归一", "必填"],
["city", "region_code", "GB/T 2260", "必填"],
["salary", "salary_min/max", "统一税前月薪", "可选"],
["description", "job_description", "去联系方式", "必填"],
["publish_time", "published_at", "ISO-8601", "可选"],
["detail_url", "source_url", "绝对URL", "必填"],
];
return `
<div class="drawer-contract-banner">
<span>FIELD MAPPING · ${h(source.schema)}</span>
<strong>${h(source.name)} → ${h(source.contract)}</strong>
<small>映射版本与连接器代码独立版本化,支持回放历史原始数据进行回归验证。</small>
</div>
<article class="drawer-card">
<h3>核心字段映射</h3>
<div class="table-wrap">
<table class="mapping-table">
<thead><tr><th>源字段</th><th>目标字段</th><th>转换规则</th><th>要求</th></tr></thead>
<tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${h(cell)}</td>`).join("")}</tr>`).join("")}</tbody>
</table>
</div>
</article>
<article class="drawer-card">
<h3>样本回归</h3>
<p>最近回归样本 1,000 条,结构通过率 100%,必填完整率 98.8%,岗位归一覆盖率 96.4%。未匹配岗位进入人工复核队列,不以模型推断覆盖原始事实。</p>
</article>
`;
}
function renderSourceRuntime(source) {
return `
${drawerStats(source)}
<article class="drawer-card">
<h3>最近运行事件</h3>
<div class="runtime-list">
${source.runtime.map(([time, title, detail]) => `<span><strong>${h(time)} · ${h(title)}</strong><small>${h(detail)}</small></span>`).join("")}
</div>
</article>
<article class="drawer-card">
<h3>稳定版本保护</h3>
<p>运行中的采集、解析和质量任务只写入暂存版本。完整规则通过并完成发布审批后,才更新供下游分析引用的稳定别名。</p>
</article>
`;
}
function renderSourceAudit(source) {
return `
<div class="drawer-contract-banner">
<span>TRACEABLE SOURCE ASSET</span>
<strong>${h(source.version)} · ${h(source.codeVersion)}</strong>
<small>来源、代码、任务、原始快照、规则、质量和交付版本均可通过追踪编号关联。</small>
</div>
<article class="drawer-card">
<h3>当前审计证据</h3>
<div class="audit-evidence-list">
<span><strong>来源注册记录</strong><small>${h(source.domain)} · 资产责任人 ${h(source.owner)}</small></span>
<span><strong>连接器代码版本</strong><small>${h(source.repo)} · ${h(source.codeVersion)}</small></span>
<span><strong>原始数据版本</strong><small>RAW-${h(source.id.toUpperCase())}-20260823 · SHA-256 已固化</small></span>
<span><strong>字段映射与契约</strong><small>${h(source.schema)} → ${h(source.contract)}</small></span>
<span><strong>生产运行策略</strong><small>${h(source.rate)} · ${h(source.retry)}</small></span>
</div>
</article>
<article class="drawer-card">
<h3>数据使用边界</h3>
<p>平台只向产教分析产品交付经契约治理的岗位信号、企业画像和统计结果。原始层与交付层分区存储,租户交付范围由 Manifest 和平台数据范围策略共同控制。</p>
</article>
`;
}
function updateSourceDrawer() {
const source = sourceById(state.sourceId);
document.getElementById("sourceDrawerKicker").textContent = `${source.categoryLabel.toUpperCase()} · ${source.adapter}`;
document.getElementById("sourceDrawerTitle").textContent = source.name;
document.getElementById("sourceDrawerDescription").textContent = `${source.domain} · ${source.coverage} · ${source.statusLabel}`;
document.querySelectorAll("#sourceDrawerTabs button").forEach((button) => {
button.classList.toggle("active", button.dataset.sourceTab === state.sourceTab);
});
const renderers = {
overview: renderSourceOverview,
acquisition: renderSourceAcquisition,
mapping: renderSourceMapping,
runtime: renderSourceRuntime,
audit: renderSourceAudit,
};
document.getElementById("sourceDrawerContent").innerHTML = renderers[state.sourceTab](source);
document.getElementById("sourceDrawerFooter").innerHTML = `
<button class="button secondary" data-drawer-close>关闭</button>
${source.category === "school" ? "" : `<button class="button secondary" data-source-records="${h(source.id)}">查看采集数据</button>`}
<button class="button secondary" data-source-test="${h(source.id)}">测试连接</button>
<button class="button primary" data-source-job="${h(source.id)}">创建采集任务</button>
`;
}
function openSourceDrawer(sourceId) {
state.sourceId = sourceId;
state.sourceTab = "overview";
updateSourceDrawer();
const drawer = document.getElementById("sourceDrawer");
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeSourceDrawer() {
const drawer = document.getElementById("sourceDrawer");
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function renderCollectionJobs() {
const jobs = collectionJobs.filter((job) => state.jobFilter === "all" || job.status === state.jobFilter);
document.getElementById("collectionJobTable").innerHTML = jobs
.map((job) => {
const source = sourceById(job.sourceId);
const statusTone = job.status === "attention" ? "attention" : job.status === "running" ? "running" : "";
return `
<tr>
<td><span class="job-title"><strong>${h(job.name)}</strong><small>${h(job.id)} · ${h(source.name)}</small></span></td>
<td>${h(job.scope)}</td>
<td><span class="job-strategy"><span>${h(job.strategy)}</span><small>${h(source.contract)}</small></span></td>
<td><div class="job-progress"><span><small>${job.progress ? "已完成" : "等待开始"}</small><strong>${job.progress}%</strong></span><i><b style="width:${job.progress}%"></b></i></div></td>
<td>
<span class="job-strategy"><span>${h(job.records)} 条</span><small>有效率 ${h(job.valid)}</small></span>
${job.records === "—" ? "" : `<button class="record-count-link" data-job-records="${h(job.id)}">查看本批数据</button>`}
</td>
<td><span class="status-pill ${statusTone}">${h(job.statusLabel)}</span></td>
<td><div class="row-actions"><button data-job-detail="${h(job.id)}">详情</button><button class="secondary" data-job-toggle="${h(job.id)}">${job.status === "running" ? "暂停" : "运行"}</button></div></td>
</tr>
`;
})
.join("");
if (!jobs.length) {
document.getElementById("collectionJobTable").innerHTML = `<tr><td colspan="7">当前分类暂无采集任务。</td></tr>`;
}
document.getElementById("runningJobCount").textContent = collectionJobs.filter((job) => job.status === "running").length;
}
function populateRecordSources() {
const select = document.getElementById("recordSourceFilter");
select.innerHTML = [
`<option value="all">全部外部来源</option>`,
...sources.filter((source) => source.category !== "school").map((source) => `<option value="${h(source.id)}">${h(source.name)}</option>`),
].join("");
if (!sources.some((source) => source.id === state.recordSource && source.category !== "school")) state.recordSource = "all";
select.value = state.recordSource;
}
function recordSearchMatches(values) {
const keyword = state.recordSearch.trim().toLowerCase();
return !keyword || values.filter(Boolean).join(" ").toLowerCase().includes(keyword);
}
function filteredStandardRecords() {
return standardJobRecords.filter((record) => {
const source = sourceById(record.sourceId);
const sourceMatch = state.recordSource === "all" || record.sourceId === state.recordSource;
const regionMatch = state.recordRegion === "all" || record.region === state.recordRegion;
return (
sourceMatch &&
regionMatch &&
recordSearchMatches([
record.id,
record.sourceRecordId,
record.taskId,
record.snapshotId,
record.jobTitleRaw,
record.jobTitleNormalized,
record.enterprise,
record.region,
record.workplace,
record.jobCode,
record.skills.join(" "),
source.name,
])
);
});
}
function filteredRawSnapshots() {
return allRawSnapshots.filter((snapshot) => {
const source = sourceById(snapshot.sourceId);
const sourceMatch = state.recordSource === "all" || snapshot.sourceId === state.recordSource;
return sourceMatch && recordSearchMatches([snapshot.id, snapshot.taskId, snapshot.objectPath, snapshot.sha256, snapshot.mediaType, source.name]);
});
}
function filteredQuarantineRecords() {
return quarantineRecords.filter((record) => {
const source = sourceById(record.sourceId);
const sourceMatch = state.recordSource === "all" || record.sourceId === state.recordSource;
const regionMatch = state.recordRegion === "all" || record.region === state.recordRegion;
return sourceMatch && regionMatch && recordSearchMatches([record.id, record.sourceRecordId, record.taskId, record.snapshotId, record.jobTitle, record.issue, record.rule, source.name]);
});
}
function recordSourceCell(source, secondary) {
return `
<span class="record-source-cell">
<b class="record-source-mark ${h(source.category)}">${h(source.short.slice(0, 2))}</b>
<span class="record-source-copy"><strong>${h(source.name)}</strong><small>${h(secondary)}</small></span>
</span>
`;
}
function recordSalary(record) {
return `¥${formatNumber(record.salaryMin)}–${formatNumber(record.salaryMax)} / 月`;
}
function renderStandardRecordTable(records, sourceLayer = false) {
if (!records.length) return `<div class="info-note">当前查询条件下没有匹配记录,请调整来源、地区或关键词。</div>`;
const rows = records
.map((record) => {
const source = sourceById(record.sourceId);
if (sourceLayer) {
return `
<tr>
<td>${recordSourceCell(source, record.sourceRecordId)}</td>
<td><span class="record-title"><strong>${h(record.jobTitleRaw)}</strong><small>${h(record.enterprise)}</small></span></td>
<td><span class="record-title"><strong>${h(record.sourceUrl.replace(/^https?:\/\//, ""))}</strong><small>来源详情定位</small></span></td>
<td><span class="record-time-copy"><strong>${h(record.taskId)}</strong><small>${h(record.snapshotId)}</small></span></td>
<td><span class="record-time-copy"><strong>${h(record.collectorVersion)}</strong><small>${h(record.parserVersion)}</small></span></td>
<td>${h(record.collectedAt)}</td>
<td><div class="row-actions"><button data-record-id="${h(record.id)}">查看</button></div></td>
</tr>
`;
}
return `
<tr>
<td>${recordSourceCell(source, record.sourceRecordId)}</td>
<td><span class="record-title"><strong>${h(record.jobTitleNormalized)}</strong><small>原始:${h(record.jobTitleRaw)}</small></span></td>
<td><span class="record-title"><strong>${h(record.enterprise)}</strong><small>${h(record.workplace)}</small></span></td>
<td><span class="record-title"><strong>${h(recordSalary(record))}</strong><small>${h(record.employmentType)} · ${h(record.jobCode)}</small></span></td>
<td><span class="record-time-copy"><strong>${h(record.publishedAt)}</strong><small>采集 ${h(record.collectedAt)}</small></span></td>
<td><span class="record-quality ${record.quality < 95 ? "attention" : ""}">${record.quality.toFixed(1)}</span></td>
<td><div class="row-actions"><button data-record-id="${h(record.id)}">查看</button></div></td>
</tr>
`;
})
.join("");
return `
<div class="table-wrap">
<table class="data-table record-table">
<thead>
${
sourceLayer
? "<tr><th>来源与源记录</th><th>源端岗位与企业</th><th>来源URL</th><th>任务与快照</th><th>采集器与解析器</th><th>采集时间</th><th>操作</th></tr>"
: "<tr><th>来源与源记录</th><th>标准岗位</th><th>企业与工作地点</th><th>薪资与分类</th><th>发布时间</th><th>质量分</th><th>操作</th></tr>"
}
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
function renderRawSnapshotTable(snapshots) {
if (!snapshots.length) return `<div class="info-note">当前查询条件下没有匹配的原始快照。</div>`;
return `
<div class="table-wrap">
<table class="data-table record-table">
<thead><tr><th>来源与快照</th><th>采集任务</th><th>对象格式</th><th>记录与大小</th><th>对象存储定位</th><th>固化时间</th><th>校验状态</th><th>操作</th></tr></thead>
<tbody>
${snapshots
.map((snapshot) => {
const source = sourceById(snapshot.sourceId);
return `
<tr>
<td>${recordSourceCell(source, snapshot.id)}</td>
<td>${h(snapshot.taskId)}</td>
<td>${h(snapshot.mediaType)}</td>
<td><span class="record-title"><strong>${h(snapshot.records)} 条</strong><small>${h(snapshot.size)}</small></span></td>
<td><span class="record-title"><strong>${h(snapshot.objectPath)}</strong><small>WORM 原始对象区</small></span></td>
<td>${h(snapshot.capturedAt)}</td>
<td><span class="status-pill running">${h(snapshot.status)}</span></td>
<td><div class="row-actions"><button data-snapshot-id="${h(snapshot.id)}">查看</button></div></td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
}
function renderQuarantineTable(records) {
if (!records.length) return `<div class="info-note">当前查询条件下没有匹配的隔离记录。</div>`;
return `
<div class="table-wrap">
<table class="data-table record-table">
<thead><tr><th>隔离编号</th><th>来源记录</th><th>岗位</th><th>质量问题</th><th>任务与快照</th><th>发现时间</th><th>处置状态</th><th>操作</th></tr></thead>
<tbody>
${records
.map((record) => {
const source = sourceById(record.sourceId);
return `
<tr>
<td><span class="record-time-copy"><strong>${h(record.id)}</strong><small>${h(record.rule)}</small></span></td>
<td>${recordSourceCell(source, record.sourceRecordId)}</td>
<td>${h(record.jobTitle)}</td>
<td><span class="record-title"><strong>${h(record.issue)}</strong><small>未进入稳定版本</small></span></td>
<td><span class="record-time-copy"><strong>${h(record.taskId)}</strong><small>${h(record.snapshotId)}</small></span></td>
<td>${h(record.detectedAt)}</td>
<td><span class="status-pill attention">${h(record.disposition)}</span></td>
<td><div class="row-actions"><button data-quarantine-id="${h(record.id)}">查看</button></div></td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
}
function currentRecordResults() {
if (state.recordLayer === "raw") return filteredRawSnapshots();
if (state.recordLayer === "quarantine") return filteredQuarantineRecords();
return filteredStandardRecords();
}
function renderRecords() {
document.querySelectorAll("#recordLayers [data-record-layer]").forEach((button) => {
button.classList.toggle("active", button.dataset.recordLayer === state.recordLayer);
});
document.getElementById("recordSourceFilter").value = state.recordSource;
document.getElementById("recordRegionFilter").value = state.recordRegion;
document.getElementById("recordSearch").value = state.recordSearch;
const results = currentRecordResults();
const renderers = {
standard: () => renderStandardRecordTable(results),
source: () => renderStandardRecordTable(results, true),
raw: () => renderRawSnapshotTable(results),
quarantine: () => renderQuarantineTable(results),
};
document.getElementById("recordTableContainer").innerHTML = renderers[state.recordLayer]();
const layerLabel = { standard: "标准岗位", source: "源端记录", raw: "原始快照", quarantine: "隔离记录" }[state.recordLayer];
document.getElementById("recordQuerySummary").textContent = `${layerLabel} · 当前展示 ${results.length} 条结构样例`;
}
function openRecordExplorer(sourceId = "all", search = "", layer = "standard") {
state.recordSource = sources.some((source) => source.id === sourceId && source.category !== "school") ? sourceId : "all";
state.recordSearch = search;
state.recordRegion = "all";
state.recordLayer = layer;
populateRecordSources();
renderRecords();
switchView("records");
}
function openRecordDrawer({ kicker, title, description, content, footer }) {
document.getElementById("recordDrawerKicker").textContent = kicker;
document.getElementById("recordDrawerTitle").textContent = title;
document.getElementById("recordDrawerDescription").textContent = description;
document.getElementById("recordDrawerContent").innerHTML = content;
document.getElementById("recordDrawerFooter").innerHTML = footer;
const drawer = document.getElementById("recordDrawer");
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeRecordDrawer() {
const drawer = document.getElementById("recordDrawer");
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function openStandardRecord(recordId) {
const record = standardJobRecords.find((item) => item.id === recordId);
if (!record) return;
const source = sourceById(record.sourceId);
const payload = {
record_id: record.id,
source_platform: record.sourceId.toUpperCase(),
source_record_id: record.sourceRecordId,
source_url: record.sourceUrl,
collector_task_id: record.taskId,
collector_version: record.collectorVersion,
raw_snapshot_id: record.snapshotId,
collected_at: record.collectedAt,
published_at: record.publishedAt,
job_title_raw: record.jobTitleRaw,
job_title_normalized: record.jobTitleNormalized,
job_code: record.jobCode,
job_description: record.description,
enterprise_name: record.enterprise,
region_code: record.regionCode,
workplace: record.workplace,
salary_min: record.salaryMin,
salary_max: record.salaryMax,
salary_currency: "CNY",
salary_period: "month",
skills: record.skills,
content_hash: record.contentHash,
parsing_version: record.parserVersion,
normalized_at: record.normalizedAt,
data_version: record.dataVersion,
};
openRecordDrawer({
kicker: `STANDARD RECORD · ${record.dataVersion}`,
title: record.jobTitleNormalized,
description: `${source.name} · ${record.sourceRecordId} · ${record.status}`,
content: `
<div class="record-detail-banner">
<div><span>${h(source.name)} · ${h(record.jobCode)}</span><strong>${h(record.jobTitleRaw)} → ${h(record.jobTitleNormalized)}</strong><small>${h(record.enterprise)} · ${h(record.workplace)} · ${h(recordSalary(record))}</small></div>
<strong class="record-detail-score">${record.quality.toFixed(1)}</strong>
</div>
<article class="drawer-card">
<h3>标准业务字段</h3>
<dl>
<div><dt>记录标识</dt><dd>${h(record.id)}</dd></div>
<div><dt>企业与岗位</dt><dd>${h(record.enterprise)} · ${h(record.jobTitleNormalized)}</dd></div>
<div><dt>工作地点</dt><dd>${h(record.workplace)} · ${h(record.regionCode)}</dd></div>
<div><dt>薪资与性质</dt><dd>${h(recordSalary(record))} · ${h(record.employmentType)}</dd></div>
<div><dt>技能标签</dt><dd><span class="evidence-tags">${record.skills.map((skill) => `<span>${h(skill)}</span>`).join("")}</span></dd></div>
<div><dt>来源定位</dt><dd><a class="record-source-link" href="${h(record.sourceUrl)}" target="_blank" rel="noopener noreferrer">${h(record.sourceUrl)}</a></dd></div>
</dl>
</article>
<article class="drawer-card">
<h3>全链路证据</h3>
<div class="record-lineage">
<span><b>01</b><span><strong>采集任务</strong><small>${h(record.taskId)} · ${h(record.collectorVersion)}</small></span></span>
<span><b>02</b><span><strong>原始快照</strong><small>${h(record.snapshotId)} · payload SHA-256 ${h(record.payloadHash)}</small></span></span>
<span><b>03</b><span><strong>解析映射</strong><small>${h(record.parserVersion)} · ${h(record.sourceRecordId)}</small></span></span>
<span><b>04</b><span><strong>质量与归一</strong><small>质量分 ${record.quality.toFixed(1)} · 岗位代码 ${h(record.jobCode)}</small></span></span>
<span><b>05</b><span><strong>稳定版本</strong><small>${h(record.dataVersion)} · content SHA-256 ${h(record.contentHash)}</small></span></span>
</div>
</article>
<article class="drawer-card">
<h3>JOB-SIGNAL V3.0 契约记录</h3>
<pre class="record-payload">${h(JSON.stringify(payload, null, 2))}</pre>
</article>
`,
footer: `
<button class="button secondary" data-record-close>关闭</button>
<button class="button secondary" data-record-source="${h(record.sourceId)}">查看数据源资产</button>
<button class="button primary" data-record-raw="${h(record.snapshotId)}" data-record-source-id="${h(record.sourceId)}">定位原始快照</button>
`,
});
}
function openRawSnapshot(snapshotId) {
const snapshot = allRawSnapshots.find((item) => item.id === snapshotId);
if (!snapshot) return;
const source = sourceById(snapshot.sourceId);
const manifest = {
snapshot_id: snapshot.id,
source_id: snapshot.sourceId,
collector_task_id: snapshot.taskId,
captured_at: snapshot.capturedAt,
media_type: snapshot.mediaType,
record_count: Number(snapshot.records.replaceAll(",", "")),
object_path: snapshot.objectPath,
compressed_size: snapshot.size,
sha256: snapshot.sha256,
retention_policy: source.retention,
immutable: true,
};
openRecordDrawer({
kicker: "RAW SNAPSHOT · IMMUTABLE",
title: snapshot.id,
description: `${source.name} · ${snapshot.records} 条 · ${snapshot.status}`,
content: `
<div class="record-detail-banner">
<div><span>${h(source.name)} · ${h(snapshot.mediaType)}</span><strong>${h(snapshot.records)} 条原始记录</strong><small>${h(snapshot.objectPath)} · ${h(snapshot.size)}</small></div>
<strong class="record-detail-score">RAW</strong>
</div>
<article class="drawer-card">
<h3>原始对象元数据</h3>
<dl>
<div><dt>采集任务</dt><dd>${h(snapshot.taskId)}</dd></div>
<div><dt>固化时间</dt><dd>${h(snapshot.capturedAt)}</dd></div>
<div><dt>对象格式</dt><dd>${h(snapshot.mediaType)}</dd></div>
<div><dt>对象定位</dt><dd>${h(snapshot.objectPath)}</dd></div>
<div><dt>SHA-256</dt><dd>${h(snapshot.sha256)}</dd></div>
<div><dt>留存策略</dt><dd>${h(source.retention)}</dd></div>
</dl>
</article>
<article class="drawer-card">
<h3>快照清单</h3>
<pre class="record-payload">${h(JSON.stringify(manifest, null, 2))}</pre>
</article>
`,
footer: `
<button class="button secondary" data-record-close>关闭</button>
<button class="button primary" data-record-source="${h(snapshot.sourceId)}">查看数据源资产</button>
`,
});
}
function openQuarantineRecord(recordId) {
const record = quarantineRecords.find((item) => item.id === recordId);
if (!record) return;
const source = sourceById(record.sourceId);
openRecordDrawer({
kicker: `QUARANTINE · ${record.rule}`,
title: record.jobTitle,
description: `${source.name} · ${record.sourceRecordId} · 未进入稳定版本`,
content: `
<div class="record-detail-banner">
<div><span>${h(record.rule)} · 质量隔离</span><strong>${h(record.issue)}</strong><small>${h(record.disposition)} · 发现于 ${h(record.detectedAt)}</small></div>
<strong class="record-detail-score">隔离</strong>
</div>
<article class="drawer-card">
<h3>隔离处置明细</h3>
<dl>
<div><dt>隔离编号</dt><dd>${h(record.id)}</dd></div>
<div><dt>来源记录</dt><dd>${h(record.sourceRecordId)}</dd></div>
<div><dt>质量问题</dt><dd>${h(record.issue)}</dd></div>
<div><dt>命中规则</dt><dd>${h(record.rule)}</dd></div>
<div><dt>当前处置</dt><dd>${h(record.disposition)}</dd></div>
<div><dt>工作地区</dt><dd>${h(record.region)}</dd></div>
</dl>
</article>
<article class="drawer-card">
<h3>来源证据</h3>
<div class="record-lineage">
<span><b>01</b><span><strong>采集任务</strong><small>${h(record.taskId)}</small></span></span>
<span><b>02</b><span><strong>原始快照</strong><small>${h(record.snapshotId)}</small></span></span>
<span><b>03</b><span><strong>质量规则</strong><small>${h(record.rule)} · ${h(record.issue)}</small></span></span>
<span><b>04</b><span><strong>隔离区</strong><small>不覆盖稳定版本,修复后按原任务参数重放</small></span></span>
</div>
</article>
`,
footer: `
<button class="button secondary" data-record-close>关闭</button>
<button class="button primary" data-record-source="${h(record.sourceId)}">查看数据源资产</button>
`,
});
}
function renderContract() {
const contract = contracts[state.contractKey];
document.querySelectorAll("#contractTabs button").forEach((button) => {
button.classList.toggle("active", button.dataset.contract === state.contractKey);
});
document.getElementById("contractContent").innerHTML = `
<div class="contract-hero">
<article class="panel contract-overview">
<span>${h(contract.key)} · ${h(contract.version)}</span>
<h2>${h(contract.title)}</h2>
<p>${h(contract.description)}</p>
<div class="contract-meta-grid">
<span><small>字段总数</small><strong>${contract.fields} 个</strong></span>
<span><small>必填字段</small><strong>${contract.required} 个</strong></span>
<span><small>标准责任人</small><strong>${h(contract.owner)}</strong></span>
</div>
</article>
<article class="panel contract-release">
<span>CONTRACT RELEASE</span>
<strong>${h(contract.status)}</strong>
<small>${h(contract.release)} · ${h(contract.compatibility)}</small>
<div class="release-steps"><span class="done">起草</span><span class="done">评审</span><span class="done">测试</span><span class="${contract.status.includes("已发布") ? "done" : ""}">发布</span></div>
</article>
</div>
<div class="contract-body-grid">
<article class="panel field-panel">
<div class="panel-head"><div><span>字段定义</span><h2>核心字段预览</h2></div><a class="text-button" href="${h(contract.schemaPath)}" download>下载 JSON Schema</a></div>
<div class="table-wrap">
<table class="field-table">
<thead><tr><th>字段</th><th>类型</th><th>必填</th><th>业务说明</th><th>约束/代码</th></tr></thead>
<tbody>${contract.fieldsPreview
.map(
([field, type, required, description, rule]) =>
`<tr><td><code class="field-code">${h(field)}</code></td><td>${h(type)}</td><td class="${required === "是" ? "required-mark" : ""}">${h(required)}</td><td>${h(description)}</td><td>${h(rule)}</td></tr>`,
)
.join("")}</tbody>
</table>
</div>
</article>
<article class="panel contract-rules-panel">
<div class="panel-head"><div><span>执行规则</span><h2>契约控制点</h2></div><em>${contract.rules.length} 组</em></div>
<div class="contract-rule-list">${contract.rules.map(([title, detail]) => `<span><strong>${h(title)}</strong>${h(detail)}</span>`).join("")}</div>
</article>
</div>
`;
}
function renderExchangeLogs() {
document.getElementById("exchangeLogTable").innerHTML = exchangeLogs
.map(
(row) => `
<tr>
${row
.map((cell, index) => `<td>${index === 6 ? `<span class="status-pill ${String(cell).includes("隔离") ? "attention" : ""}">${h(cell)}</span>` : h(cell)}</td>`)
.join("")}
</tr>
`,
)
.join("");
}
function renderQuality() {
document.getElementById("qualityRuleList").innerHTML = qualityRules
.map(
([code, title, detail, score, status]) => `
<span class="rule-item"><b>${h(code)}</b><span><strong>${h(title)}</strong><small>${h(detail)}</small></span><em>${h(score)} · ${h(status)}</em></span>
`,
)
.join("");
document.getElementById("normalizationList").innerHTML = normalizationItems
.map(
([raw, target, confidence], index) => `
<span class="normalization-item">
<span><strong>${h(raw)}</strong><small>来源岗位别名</small></span><i>→</i>
<span><strong>${h(target)}</strong><small>建议映射 · 置信度 ${h(confidence)}</small></span>
<button data-normalize-index="${index}">确认映射</button>
</span>
`,
)
.join("");
if (!normalizationItems.length) {
document.getElementById("normalizationList").innerHTML = `<div class="info-note">当前高优先级待审项已处理完成,其余低优先级项将在下一批次继续复核。</div>`;
}
}
function renderAudit() {
const mergedEvents = [
...operationsStore.auditEvents.map((event) => ({ ...event, time: operationTime(event.time) })),
...auditEvents,
];
const events = mergedEvents.filter((event) => state.auditFilter === "all" || event.type === state.auditFilter);
document.getElementById("auditTable").innerHTML = events
.map(
(event) => `
<tr>
<td><span class="audit-event"><strong>${h(event.time)}</strong><small>${h(event.id)}</small></span></td>
<td>${h(event.event)}</td>
<td>${h(event.object)}</td>
<td>${h(event.actor)}</td>
<td><span class="status-pill ${event.result.includes("保护") || event.result.includes("待审") ? "attention" : ""}">${h(event.result)}</span></td>
<td><div class="evidence-tags">${event.evidence.map((item) => `<span>${h(item)}</span>`).join("")}</div></td>
</tr>
`,
)
.join("");
}
function openInfoModal(kicker, title, content) {
document.getElementById("infoModalKicker").textContent = kicker;
document.getElementById("infoModalTitle").textContent = title;
document.getElementById("infoModalContent").innerHTML = content;
openModal(document.getElementById("infoModal"));
}
function csvEscape(value) {
const text = String(value ?? "");
return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}
function downloadText(filename, content, type = "text/plain;charset=utf-8") {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 500);
}
function downloadJson(filename, value) {
downloadText(filename, JSON.stringify(value, null, 2), "application/json;charset=utf-8");
}
function exportSourceCatalog() {
const headers = ["source_id", "name", "category", "domain", "adapter", "mode", "schedule", "status", "contract", "owner", "version"];
const rows = sources.map((source) => [
source.id,
source.name,
source.category,
source.domain,
source.adapter,
source.mode,
source.schedule,
source.statusLabel,
source.contract,
source.owner,
source.version,
]);
const csv = [headers, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
downloadText("edu-data-source-catalog-20260823.csv", `\uFEFF${csv}`, "text/csv;charset=utf-8");
showToast(`已导出${sources.length}个数据源及其连接器、契约、责任人与生产版本`, "数据源目录已生成");
}
function candidateManifestInputFingerprint() {
const release = currentRelease();
const disposition = latestQualityDisposition();
return JSON.stringify({
releaseId: release?.id,
dataVersion: release?.dataVersion,
product: document.getElementById("exportProduct").value,
tenant: document.getElementById("exportTenant").value,
asOf: document.getElementById("exportAsOf").value,
format: document.getElementById("exportFormat").value,
impactTenants: release?.impactTenants,
deliveries: operationsStore.deliveries.map((delivery) => [delivery.tenantId, delivery.expectedRecords, delivery.expectedSha]),
qualityDisposition: disposition
? {
id: disposition.id,
action: disposition.action,
status: disposition.status,
trace: disposition.trace,
verification: disposition.verification,
}
: null,
});
}
function buildPackageManifest(options = {}) {
const release = currentRelease();
const approval = currentApproval();
const releaseStatus = options.releaseStatus || release?.status || "DRAFT";
const approvalStatus = options.approvalStatus || approval?.status || "DRAFT";
const published = releaseStatus === "PUBLISHED";
const product = document.getElementById("exportProduct").value;
const tenantLabel = document.getElementById("exportTenant").value;
const asOf = document.getElementById("exportAsOf").value;
const formatLabel = document.getElementById("exportFormat").value;
const format = {
"Parquet + Manifest": "parquet",
"CSV UTF-8 + Manifest": "csv_utf8",
"JSONL + Manifest": "jsonl",
}[formatLabel];
const formatMeta = {
parquet: { extension: "parquet", mediaType: "application/vnd.apache.parquet", compression: "zstd", bytesPerRecord: 240 },
csv_utf8: { extension: "csv", mediaType: "text/csv", compression: "none", bytesPerRecord: 410 },
jsonl: { extension: "jsonl", mediaType: "application/x-ndjson", compression: "none", bytesPerRecord: 620 },
}[format];
const tenantDeliveries = release?.impactTenants?.length
? release.impactTenants.map((tenantId) => operationsStore.deliveries.find((delivery) => delivery.tenantId === tenantId)).filter(Boolean)
: [];
const tenantScope = tenantDeliveries.length ? tenantDeliveries.map((delivery) => delivery.tenantId) : [tenantLabel];
const tenantDataFiles = tenantDeliveries.length
? tenantDeliveries.map((delivery) => ({
path: `data/${delivery.tenantId}/job-signal.${formatMeta.extension}`,
media_type: formatMeta.mediaType,
bytes: delivery.expectedRecords * formatMeta.bytesPerRecord,
sha256: delivery.expectedSha,
record_count: delivery.expectedRecords,
compression: formatMeta.compression,
}))
: [
{
path: `data/job-signal.${formatMeta.extension}`,
media_type: formatMeta.mediaType,
bytes: 2846320 * formatMeta.bytesPerRecord,
sha256: "4f8d91a47e2b6d82b2aaad136d344d61c83f9cd472185d2543725c91870e9e32",
record_count: 2846320,
compression: formatMeta.compression,
},
];
const recordCount = tenantDataFiles.reduce((total, file) => total + file.record_count, 0);
return {
manifest_version: "1.2.0",
package_id: release?.packageId || `EDU-PKG-${asOf.replaceAll("-", "")}-CANDIDATE`,
...(release ? { release_id: release.id, release_status: releaseStatus } : {}),
data_product: product,
data_version: release?.dataVersion || `V${asOf.replaceAll("-", ".")}`,
contract_name: "JOB-SIGNAL",
contract_version: "3.0.0",
data_as_of: asOf,
tenant_scope: tenantScope,
output_format: format,
record_count: recordCount,
quality_score: 94.7,
lineage_snapshot: `LINEAGE-${asOf.replaceAll("-", "")}-09`,
previous_stable_version: release?.previousStableVersion || "V2026.08.22",
files: [
...tenantDataFiles,
{
path: "schema/external-job-source.schema.json",
media_type: "application/schema+json",
bytes: 18420,
sha256: "96c2be742dc43f8e7ee526db6358030873406c450f15e67225c605dc39b431a8",
record_count: null,
compression: "none",
},
{
path: "quality/quality-report.json",
media_type: "application/json",
bytes: 32640,
sha256: "c2d8f733e6711219823110d7d35fdc5535f133ed60f60950f9dc78a228cb402d",
record_count: null,
compression: "none",
},
{
path: "lineage/source-lineage.json",
media_type: "application/json",
bytes: 84210,
sha256: "66e102b99bf056868670ac971011740c6f66f62b86ea43cc66326ed9a0a5807e",
record_count: null,
compression: "none",
},
],
quality_report_path: "quality/quality-report.json",
generated_at: new Date().toISOString(),
generated_by: "陈致远",
approval_reference: approval ? `${approval.id}:${approvalStatus}` : null,
notes: published
? "该文件是独立审批通过后生成的发布 Manifest;静态原型只记录本地稳定版本状态,不会向学校真实发送。"
: "该文件仅为候选包 Manifest;只有独立审批通过并执行发布后才成为稳定版本,静态原型不会向学校真实发送。",
};
}
function generateCandidateManifest() {
const release = currentRelease();
updateReleaseQualityGate();
if (!release || release.status !== "DRAFT") {
showToast("候选已进入审批或发布流程,Manifest 已冻结;驳回后需返回草稿重建", "无法重建候选 Manifest");
return;
}
if (release.qualityGate !== "PASS") {
showToast("只有 FIX_REPLAY 的完整验证证据通过后才能生成候选 Manifest", "质量门禁阻断");
return;
}
const inputFingerprint = candidateManifestInputFingerprint();
if (release.candidateManifest && release.candidateInputFingerprint === inputFingerprint) {
downloadJson(`${release.candidateManifest.package_id}-candidate-manifest.json`, release.candidateManifest);
showToast("候选输入与质量证据未变化,已复用原 Manifest;未新增审计或交换记录", "候选 Manifest 已复用");
return;
}
const manifest = buildPackageManifest();
const metadata = addOperationsAudit({
type: "publish",
event: "生成候选包 Manifest",
object: `${release.id} · ${release.packageId}`,
result: "候选已生成",
evidence: ["Manifest", "质量报告", "血缘快照", "SHA-256"],
});
release.manifestGeneratedAt = metadata.time;
release.candidateManifest = manifest;
release.candidateInputFingerprint = inputFingerprint;
release.actor = metadata.actor;
release.time = metadata.time;
release.trace = metadata.trace;
release.evidence = [...metadata.evidence];
saveOperationsStore();
downloadJson(`${manifest.package_id}-candidate-manifest.json`, manifest);
exchangeLogs.unshift([manifest.package_id, "候选导出", manifest.data_product, manifest.data_version, formatNumber(manifest.record_count), "陈致远", "候选已生成", "刚刚"]);
exchangeLogs.splice(OPERATIONS_LIMITS.exchangeLogs);
renderExchangeLogs();
renderOperations();
renderAudit();
showToast(`${manifest.package_id} 仅生成候选文件,尚未发布或向学校发送`, "候选包 / Manifest 已生成");
}
function exportDailyReport() {
const report = {
report_id: "OPS-DAILY-20260823",
generated_at: new Date().toISOString(),
environment: "production-east-cn",
batch: "EDU-JOB-20260823",
source_count: sources.length,
running_sources: sources.filter((source) => source.status === "running").length,
running_jobs: collectionJobs.filter((job) => job.status === "running").length,
records_collected_today: sources.reduce((sum, source) => sum + source.today, 0),
quality_score: 94.7,
normalization_coverage: 96.4,
stable_data_version: "V2026.08.23",
tenant_delivery_count: 36,
attention_items: [
"27个岗位别名待人工归一",
"杭州人才网处于自动降速保护",
"SCHOOL-EMPLOYMENT V2.2待生产发布",
],
};
downloadJson("edu-data-operations-daily-20260823.json", report);
showToast("运行日报包含生产批次、来源、采集、质量、归一和交付状态", "运行日报已生成");
}
function exportExchangeLog() {
const headers = ["trace_id", "type", "object", "version", "records", "actor", "result", "time"];
const csv = [headers, ...exchangeLogs].map((row) => row.map(csvEscape).join(",")).join("\n");
downloadText("edu-data-exchange-log-20260823.csv", `\uFEFF${csv}`, "text/csv;charset=utf-8");
showToast("导入与交付记录已按当前筛选范围导出", "交换记录已生成");
}
function exportAuditLog() {
const headers = ["trace_id", "time", "type", "event", "object", "actor", "result", "evidence"];
const rows = [...operationsStore.auditEvents, ...auditEvents].map((event) => [
event.trace || event.id,
event.time,
event.type,
event.event,
event.object,
event.actor,
event.result,
event.evidence.join("|"),
]);
const csv = [headers, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
downloadText("edu-data-audit-log-20260823.csv", `\uFEFF${csv}`, "text/csv;charset=utf-8");
showToast("审计日志已携带追踪编号、操作者、结果和证据清单", "审计日志已生成");
}
function setupNavigation() {
document.querySelectorAll(".nav-item[data-view]").forEach((button) => button.addEventListener("click", () => switchView(button.dataset.view)));
document.addEventListener("click", (event) => {
const jump = event.target.closest("[data-view-jump]");
if (jump) switchView(jump.dataset.viewJump);
const issueButton = event.target.closest("[data-open-issue]");
if (issueButton) {
const issueId = issueButton.dataset.openIssue;
const modal = issueButton.closest(".modal-backdrop");
if (modal) closeModal(modal);
state.operationsTab = "issues";
renderOperations();
switchView("operations");
openIssueDrawer(issueId);
}
const operationsJump = event.target.closest("[data-operations-jump]");
if (operationsJump) {
const operationsTab = operationsJump.dataset.operationsJump;
const modal = operationsJump.closest(".modal-backdrop");
if (modal) closeModal(modal);
state.operationsTab = operationsTab;
renderOperations();
switchView("operations");
}
const sourceButton = event.target.closest("[data-source-detail]");
if (sourceButton) openSourceDrawer(sourceButton.dataset.sourceDetail);
const sourceRecordsButton = event.target.closest("[data-source-records]");
if (sourceRecordsButton) {
if (document.getElementById("sourceDrawer").classList.contains("open")) closeSourceDrawer();
openRecordExplorer(sourceRecordsButton.dataset.sourceRecords);
}
const sourceModalButton = event.target.closest("[data-open-source-modal]");
if (sourceModalButton) openModal(document.getElementById("sourceModal"));
});
document.getElementById("mobileMenu").addEventListener("click", () => document.body.classList.add("sidebar-open"));
document.getElementById("sidebarScrim").addEventListener("click", () => document.body.classList.remove("sidebar-open"));
document.getElementById("globalSearch").addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
state.sourceSearch = event.currentTarget.value;
document.getElementById("sourceSearch").value = state.sourceSearch;
switchView("sources");
renderSources();
});
}
function setupSourceRegistry() {
document.getElementById("sourceFilters").addEventListener("click", (event) => {
const button = event.target.closest("[data-source-filter]");
if (!button) return;
state.sourceFilter = button.dataset.sourceFilter;
document.querySelectorAll("[data-source-filter]").forEach((item) => item.classList.toggle("active", item === button));
renderSources();
});
document.getElementById("sourceSearch").addEventListener("input", (event) => {
state.sourceSearch = event.target.value;
renderSources();
});
document.getElementById("sourceCatalogExport").addEventListener("click", exportSourceCatalog);
document.getElementById("sourceForm").addEventListener("submit", (event) => {
event.preventDefault();
const name = document.getElementById("newSourceName").value.trim();
const domainInput = document.getElementById("newSourceDomain").value.trim();
let domain;
try {
const url = new URL(domainInput);
if (!["http:", "https:", "jdbc:", "sftp:"].includes(url.protocol)) throw new Error("unsupported");
domain = url.host || domainInput;
} catch {
showToast("请输入有效的 HTTP、HTTPS、JDBC 或 SFTP 数据源地址", "数据源地址无效");
return;
}
if (sources.some((source) => source.domain === domain)) {
showToast("该入口已登记,请进入现有数据源创建新版本或采集任务", "数据源已存在");
return;
}
const category = document.getElementById("newSourceCategory").value;
const categoryLabel = { commercial: "商业平台", public: "公共平台", school: "校内系统" }[category];
const source = {
id: `source-${Date.now()}`,
name,
short: name.slice(0, 2),
category,
categoryLabel,
domain,
adapter: `Custom-Collector 1.0`,
mode: document.getElementById("newSourceMode").value,
schedule: document.getElementById("newSourceSchedule").value,
status: "running",
statusLabel: "连接测试",
health: 100,
today: 0,
total: "0",
updated: "刚刚",
coverage: document.getElementById("newSourceScope").value.trim(),
contract: document.getElementById("newSourceContract").value,
schema: "CUSTOM-SOURCE 1.0",
version: `SRC-CUSTOM-${new Date().toISOString().slice(0, 10).replaceAll("-", "")}-01`,
owner: document.getElementById("newSourceOwner").value.trim(),
retention: "原始数据 180 天 · 解析结果 36 个月",
rate: "初次样本验证 · 1 分片",
retry: "指数退避 · 最多 3 次",
change: "新数据源已注册,等待小样本连接测试和字段映射确认。",
repo: "connector-registry/custom-collector",
codeVersion: "draft/1.0.0 · pending",
runtime: [["刚刚", "数据源完成注册", "连接测试任务已创建"]],
};
sources.unshift(source);
populateJobSources();
populateRecordSources();
collectionJobs.unshift({
id: `JOB-${Date.now()}`,
name: `${name}连接与样本预检`,
sourceId: source.id,
scope: source.coverage,
strategy: "样本验证 · 1分片",
progress: 0,
records: "—",
valid: "—",
status: "scheduled",
statusLabel: "等待调度",
});
event.currentTarget.reset();
closeModal(document.getElementById("sourceModal"));
renderSources();
renderOverviewSources();
renderCollectionJobs();
switchView("sources");
showToast(`${name}已进入数据源注册中心,连接测试任务已创建`, "数据源注册成功");
});
}
function setupSourceDrawer() {
document.getElementById("closeSourceDrawer").addEventListener("click", closeSourceDrawer);
document.getElementById("sourceDrawer").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeSourceDrawer();
});
document.getElementById("sourceDrawerTabs").addEventListener("click", (event) => {
const button = event.target.closest("[data-source-tab]");
if (!button) return;
state.sourceTab = button.dataset.sourceTab;
updateSourceDrawer();
});
document.getElementById("sourceDrawerFooter").addEventListener("click", (event) => {
if (event.target.closest("[data-drawer-close]")) {
closeSourceDrawer();
return;
}
const test = event.target.closest("[data-source-test]");
if (test) {
const source = sourceById(test.dataset.sourceTest);
test.disabled = true;
test.textContent = "正在测试…";
setTimeout(() => {
test.disabled = false;
test.textContent = "测试连接";
showToast(`${source.name}入口可达,样本解析和契约映射验证通过`, "连接测试通过");
}, 900);
return;
}
const jobButton = event.target.closest("[data-source-job]");
if (jobButton) {
const source = sourceById(jobButton.dataset.sourceJob);
closeSourceDrawer();
document.getElementById("jobSource").value = source.id;
document.getElementById("jobName").value = `${source.name}增量采集`;
openModal(document.getElementById("jobModal"));
}
});
}
function setupCollectionJobs() {
document.getElementById("jobFilters").addEventListener("click", (event) => {
const button = event.target.closest("[data-job-filter]");
if (!button) return;
state.jobFilter = button.dataset.jobFilter;
document.querySelectorAll("[data-job-filter]").forEach((item) => item.classList.toggle("active", item === button));
renderCollectionJobs();
});
document.getElementById("collectionJobTable").addEventListener("click", (event) => {
const toggle = event.target.closest("[data-job-toggle]");
if (toggle) {
const job = collectionJobs.find((item) => item.id === toggle.dataset.jobToggle);
if (!job) return;
if (job.status === "running") {
job.status = "scheduled";
job.statusLabel = "已暂停";
} else {
job.status = "running";
job.statusLabel = "运行中";
if (!job.progress) job.progress = 4;
}
renderCollectionJobs();
showToast(`${job.name}当前状态:${job.statusLabel}`, "任务状态已更新");
return;
}
const detail = event.target.closest("[data-job-detail]");
if (detail) {
const job = collectionJobs.find((item) => item.id === detail.dataset.jobDetail);
const source = sourceById(job.sourceId);
openInfoModal(
"COLLECTION JOB DETAIL",
job.name,
`<div class="info-grid"><span>追踪编号<strong>${h(job.id)}</strong></span><span>数据来源<strong>${h(source.name)}</strong></span><span>任务范围<strong>${h(job.scope)}</strong></span><span>运行策略<strong>${h(job.strategy)}</strong></span><span>当前进度<strong>${job.progress}%</strong></span><span>目标契约<strong>${h(source.contract)}</strong></span></div><div class="info-note">任务绑定连接器 ${h(source.codeVersion)}、源版本 ${h(source.version)} 和原始快照。失败分片进入隔离队列,可按原任务参数重放。</div>`,
);
return;
}
const records = event.target.closest("[data-job-records]");
if (records) {
const job = collectionJobs.find((item) => item.id === records.dataset.jobRecords);
if (!job) return;
openRecordExplorer(job.sourceId, job.id, "source");
}
});
document.getElementById("newCollectionJob").addEventListener("click", () => openModal(document.getElementById("jobModal")));
document.getElementById("jobForm").addEventListener("submit", (event) => {
event.preventDefault();
const sourceId = document.getElementById("jobSource").value;
const job = {
id: `JOB-${Date.now()}`,
name: document.getElementById("jobName").value.trim(),
sourceId,
scope: `${document.getElementById("jobRegion").value.trim()} · ${document.getElementById("jobScope").value.trim()}`,
strategy: `${document.getElementById("jobMode").value} · 预检分片`,
progress: document.getElementById("jobSchedule").value === "立即执行" ? 4 : 0,
records: "—",
valid: "—",
status: document.getElementById("jobSchedule").value === "立即执行" ? "running" : "scheduled",
statusLabel: document.getElementById("jobSchedule").value === "立即执行" ? "预检中" : "等待调度",
};
collectionJobs.unshift(job);
closeModal(document.getElementById("jobModal"));
renderCollectionJobs();
switchView("collectors");
showToast(`${job.name}已创建,任务先执行小样本预检`, "采集任务已进入队列");
});
document.getElementById("pauseAllJobs").addEventListener("click", (event) => {
const resume = event.currentTarget.dataset.mode === "resume";
collectionJobs.forEach((job) => {
if (resume && job.status === "scheduled") {
job.status = "running";
job.statusLabel = "运行中";
} else if (!resume && job.status === "running") {
job.status = "scheduled";
job.statusLabel = "已暂停";
}
});
event.currentTarget.dataset.mode = resume ? "" : "resume";
event.currentTarget.textContent = resume ? "暂停全部" : "恢复全部";
renderCollectionJobs();
showToast(resume ? "已恢复所有可运行采集任务" : "运行中任务已安全暂停并保存水位", "调度器状态已更新");
});
}
function exportCurrentRecords() {
const results = currentRecordResults();
if (!results.length) {
showToast("当前查询条件下没有可导出的记录", "没有查询结果");
return;
}
if (state.recordLayer === "raw") {
downloadJson("raw-snapshot-query-20260823.json", {
query: { source_id: state.recordSource, keyword: state.recordSearch },
stable_version: "V2026.08.23",
records: results,
});
} else if (state.recordLayer === "quarantine") {
const headers = ["quarantine_id", "source_id", "source_record_id", "job_title", "issue", "rule", "detected_at", "disposition", "task_id", "snapshot_id"];
const rows = results.map((record) => [record.id, record.sourceId, record.sourceRecordId, record.jobTitle, record.issue, record.rule, record.detectedAt, record.disposition, record.taskId, record.snapshotId]);
const csv = [headers, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
downloadText("quarantine-record-query-20260823.csv", `\uFEFF${csv}`, "text/csv;charset=utf-8");
} else {
const headers =
state.recordLayer === "source"
? ["record_id", "source_id", "source_record_id", "source_url", "task_id", "snapshot_id", "job_title_raw", "enterprise_name", "collected_at", "collector_version", "parser_version"]
: ["record_id", "source_id", "source_record_id", "job_title_raw", "job_title_normalized", "job_code", "enterprise_name", "region_code", "workplace", "salary_min", "salary_max", "published_at", "collected_at", "quality_score", "content_hash", "data_version"];
const rows = results.map((record) =>
state.recordLayer === "source"
? [record.id, record.sourceId, record.sourceRecordId, record.sourceUrl, record.taskId, record.snapshotId, record.jobTitleRaw, record.enterprise, record.collectedAt, record.collectorVersion, record.parserVersion]
: [record.id, record.sourceId, record.sourceRecordId, record.jobTitleRaw, record.jobTitleNormalized, record.jobCode, record.enterprise, record.regionCode, record.workplace, record.salaryMin, record.salaryMax, record.publishedAt, record.collectedAt, record.quality, record.contentHash, record.dataVersion],
);
const csv = [headers, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
downloadText(`${state.recordLayer}-job-record-query-20260823.csv`, `\uFEFF${csv}`, "text/csv;charset=utf-8");
}
showToast(`已按当前条件导出 ${results.length} 条${{ standard: "标准岗位", source: "源端记录", raw: "原始快照", quarantine: "隔离记录" }[state.recordLayer]}`, "查询结果已生成");
}
function setupRecords() {
document.getElementById("recordLayers").addEventListener("click", (event) => {
const button = event.target.closest("[data-record-layer]");
if (!button) return;
state.recordLayer = button.dataset.recordLayer;
renderRecords();
});
document.getElementById("recordSourceFilter").addEventListener("change", (event) => {
state.recordSource = event.target.value;
renderRecords();
});
document.getElementById("recordRegionFilter").addEventListener("change", (event) => {
state.recordRegion = event.target.value;
renderRecords();
});
document.getElementById("recordSearch").addEventListener("input", (event) => {
state.recordSearch = event.target.value;
renderRecords();
});
document.getElementById("recordTableContainer").addEventListener("click", (event) => {
const record = event.target.closest("[data-record-id]");
if (record) {
openStandardRecord(record.dataset.recordId);
return;
}
const snapshot = event.target.closest("[data-snapshot-id]");
if (snapshot) {
openRawSnapshot(snapshot.dataset.snapshotId);
return;
}
const quarantine = event.target.closest("[data-quarantine-id]");
if (quarantine) openQuarantineRecord(quarantine.dataset.quarantineId);
});
document.getElementById("recordRefresh").addEventListener("click", () => {
renderRecords();
showToast("查询已重新执行,当前绑定稳定数据版本 V2026.08.23", "采集数据已刷新");
});
document.getElementById("recordExport").addEventListener("click", exportCurrentRecords);
document.getElementById("closeRecordDrawer").addEventListener("click", closeRecordDrawer);
document.getElementById("recordDrawer").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeRecordDrawer();
});
document.getElementById("recordDrawerFooter").addEventListener("click", (event) => {
if (event.target.closest("[data-record-close]")) {
closeRecordDrawer();
return;
}
const source = event.target.closest("[data-record-source]");
if (source) {
closeRecordDrawer();
openSourceDrawer(source.dataset.recordSource);
return;
}
const raw = event.target.closest("[data-record-raw]");
if (raw) {
state.recordSource = raw.dataset.recordSourceId;
state.recordLayer = "raw";
state.recordSearch = raw.dataset.recordRaw;
closeRecordDrawer();
populateRecordSources();
renderRecords();
showToast(`${raw.dataset.recordRaw} 已在原始快照层定位`, "已切换数据层");
}
});
}
function setupContracts() {
document.getElementById("contractTabs").addEventListener("click", (event) => {
const button = event.target.closest("[data-contract]");
if (!button) return;
state.contractKey = button.dataset.contract;
renderContract();
});
document.getElementById("downloadStandardKit").addEventListener("click", () => {
const kit = {
product: "产教数据中台标准包",
version: "2026.08",
generated_at: new Date().toISOString(),
contracts: Object.values(contracts).map((contract) => ({
name: contract.key,
version: contract.version,
status: contract.status,
schema: contract.schemaPath,
})),
dictionaries: ["GB/T 2260 地区代码", "教育部职业教育专业目录", "岗位分类词典 V4.3", "技能词典 V3.8"],
delivery_requirements: ["JSON Schema", "数据样例", "字段说明", "质量门槛", "变更日志", "Manifest"],
};
downloadJson("edu-industry-data-standard-kit-2026.08.json", kit);
showToast("标准包索引已生成,可继续下载每个契约的 JSON Schema 与样例文件", "数据标准包已下载");
});
document.getElementById("compareContractVersions").addEventListener("click", () => {
const contract = contracts[state.contractKey];
openInfoModal(
"CONTRACT VERSION DIFF",
`${contract.key} 版本变更`,
`<div class="info-grid"><span>当前版本<strong>${h(contract.version)}</strong></span><span>兼容策略<strong>${h(contract.compatibility)}</strong></span><span>发布时间<strong>${h(contract.release)}</strong></span><span>影响来源<strong>${state.contractKey === "external-job" ? "7个外部来源" : "36所学校租户"}</strong></span></div><div class="info-note">正式发布前执行 Schema 兼容性、历史样本回放、字段映射和下游影响测试。破坏性变更必须提升主版本并提供双写迁移窗口。</div>`,
);
});
}
function setupExchange() {
const input = document.getElementById("importFile");
const zone = document.getElementById("uploadZone");
const preview = document.getElementById("importPreview");
const validateButton = document.getElementById("validateImport");
const handleFile = (file) => {
if (!file) return;
input._selectedFile = file;
preview.querySelector("small").textContent = `${(file.size / 1024).toFixed(1)} KB · ${file.type || "application/octet-stream"}`;
preview.querySelector("strong").textContent = file.name;
validateButton.disabled = false;
};
input.addEventListener("change", () => handleFile(input.files[0]));
["dragenter", "dragover"].forEach((type) =>
zone.addEventListener(type, (event) => {
event.preventDefault();
zone.classList.add("dragging");
}),
);
["dragleave", "drop"].forEach((type) =>
zone.addEventListener(type, (event) => {
event.preventDefault();
zone.classList.remove("dragging");
if (type === "drop") handleFile(event.dataTransfer.files[0]);
}),
);
validateButton.addEventListener("click", async () => {
const file = input._selectedFile || input.files[0];
if (!file) return;
validateButton.disabled = true;
validateButton.textContent = "正在预检…";
let result = { passed: true, message: "文件结构可读取,已进入服务端分片与质量干跑" };
if (file.name.toLowerCase().endsWith(".csv")) {
const text = await file.text();
const [headerLine, ...lines] = text.split(/\r?\n/).filter(Boolean);
const headers = headerLine.split(",").map((item) => item.trim().replace(/^"|"$/g, ""));
const contractKey = document.getElementById("importContract").value;
const required =
contractKey === "external-job"
? ["record_id", "source_platform", "source_record_id", "source_url", "collected_at", "job_title_raw", "enterprise_name", "region_code", "content_hash"]
: ["tenant_id", "student_token", "cohort", "program_code", "employment_status", "confirmed_at", "source_system", "updated_at"];
const missing = required.filter((field) => !headers.includes(field));
result = {
passed: missing.length === 0,
message: missing.length ? `缺少必填字段:${missing.join("、")}` : `识别 ${headers.length} 个字段、${lines.length} 条样本记录,核心必填字段完整`,
};
}
preview.querySelector("small").textContent = result.passed ? "结构预检通过" : "结构预检未通过";
preview.querySelector("strong").textContent = result.message;
validateButton.disabled = false;
validateButton.textContent = result.passed ? "重新预检" : "修复后重试";
if (result.passed) {
exchangeLogs.unshift([`IMP-${Date.now()}`, "导入", file.name, "暂存版本", "样本预检", "陈致远", "预检通过", "刚刚"]);
renderExchangeLogs();
}
showToast(result.message, result.passed ? "导入预检通过" : "导入预检失败");
});
document.getElementById("buildExportPackage").addEventListener("click", () => {
document.querySelector(".export-panel").scrollIntoView({ behavior: "smooth", block: "center" });
});
document.getElementById("generatePackage").addEventListener("click", () => {
generateCandidateManifest();
});
document.getElementById("exportExchangeLog").addEventListener("click", exportExchangeLog);
}
function setupQuality() {
document.getElementById("normalizationList").addEventListener("click", (event) => {
const button = event.target.closest("[data-normalize-index]");
if (!button) return;
const index = Number(button.dataset.normalizeIndex);
const item = normalizationItems[index];
normalizationItems.splice(index, 1);
renderQuality();
showToast(`“${item[0]}”已映射到“${item[1]}”,规则将在下一批次生效`, "岗位归一已确认");
});
document.getElementById("normalizationBatch").addEventListener("click", () => {
const count = normalizationItems.length;
normalizationItems = [];
renderQuality();
showToast(`已确认 ${count} 个高置信度岗位映射,变更记录已写入审计`, "批量归一完成");
});
document.getElementById("qualityRuleButton").addEventListener("click", () => {
openInfoModal(
"QUALITY POLICY",
"生产质量规则策略",
`<div class="info-grid"><span>阻断规则<strong>12条</strong></span><span>告警规则<strong>20条</strong></span><span>综合发布阈值<strong>≥ 90</strong></span><span>规则版本<strong>QUALITY 3.6</strong></span></div><div class="info-note">阻断规则失败时任务进入隔离区,不更新稳定版本;告警规则允许带限制发布,但必须在质量报告中披露样本、影响范围和处置计划。</div>`,
);
});
}
function setupAudit() {
document.getElementById("auditFilters").addEventListener("click", (event) => {
const button = event.target.closest("[data-audit-filter]");
if (!button) return;
state.auditFilter = button.dataset.auditFilter;
document.querySelectorAll("[data-audit-filter]").forEach((item) => item.classList.toggle("active", item === button));
renderAudit();
});
document.getElementById("exportAuditLog").addEventListener("click", exportAuditLog);
document.getElementById("auditPolicyButton").addEventListener("click", () => {
openInfoModal(
"AUDIT POLICY",
"生产审计与留存策略",
`<div class="info-grid"><span>关键事件留存<strong>36个月</strong></span><span>审计存储<strong>WORM</strong></span><span>每日完整性校验<strong>SHA-256</strong></span><span>追踪覆盖率<strong>100%</strong></span></div><div class="info-note">采集请求、连接器版本、原始快照、映射规则、质量结果、审批和租户交付都写入统一 trace_id。敏感配置只记录密钥引用和变更摘要,不在日志中保存明文凭据。</div>`,
);
});
}
function setupOperations() {
document.getElementById("operationsTabs").addEventListener("click", (event) => {
const button = event.target.closest("[data-operations-tab]");
if (!button) return;
state.operationsTab = button.dataset.operationsTab;
renderOperations();
});
document.getElementById("operationsPrimaryAction").addEventListener("click", () => {
const issue = operationsStore.issues.find((item) => !["RESOLVED", "CLOSED"].includes(item.status));
if (issue) openIssueDrawer(issue.id);
else {
state.operationsTab = "releases";
renderOperations();
}
});
document.getElementById("resetOperationsSeed").addEventListener("click", () => {
operationsStore = operationsSeed();
saveOperationsStore();
closeIssueDrawer();
renderOperations();
renderAudit();
showToast("问题、工单、质量、审批、发布、交付与审计已恢复到可重复演示种子", "运营处置数据已重置");
});
document.getElementById("closeIssueDrawer").addEventListener("click", closeIssueDrawer);
document.getElementById("issueDrawer").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeIssueDrawer();
});
document.getElementById("issueDrawer").addEventListener("click", (event) => {
if (event.target.closest("[data-issue-close]")) {
closeIssueDrawer();
return;
}
const button = event.target.closest("[data-issue-action]");
if (!button) return;
const issue = operationsStore.issues.find((item) => item.id === state.issueId);
if (!issue) return;
const action = button.dataset.issueAction;
if (action === "acknowledge" && issue.status === "ALERTED") {
transitionIssue(issue, "ACKNOWLEDGED", "运营人员已确认告警、影响与SLA。", ["告警确认记录"]);
return;
}
if (action === "assign" && issue.status === "ACKNOWLEDGED") {
const assignee = document.getElementById("issueAssignee")?.value;
if (!assignee) return;
issue.owner = assignee;
transitionIssue(issue, "ASSIGNED", `已指派给 ${assignee}。`, ["责任人指派记录"]);
return;
}
if (action === "investigate" && issue.status === "ASSIGNED") {
transitionIssue(issue, "INVESTIGATING", "已锁定源端时延与解析隔离记录,开始执行排障。", ["排障工作区快照", issue.relatedTask]);
return;
}
if (action === "submit-quality" && issue.status === "INVESTIGATING") {
const dispositionAction = document.getElementById("qualityDispositionAction")?.value;
const reviewer = document.getElementById("qualityReviewer")?.value;
const reason = document.getElementById("qualityDispositionReason")?.value.trim();
if (!reason) {
showToast("必须填写根因、处置范围与复核依据", "缺少处置原因");
return;
}
if (reviewer === OPERATIONS_ACTOR) {
showToast("质量复核人必须独立于处置提交人", "复核职责未分离");
return;
}
const evidence =
dispositionAction === "FIX_REPLAY"
? ["不可变处置动作", "修复补丁 HZRC-2.7.4", "重放任务 REPLAY-HZRC-20260823-01"]
: dispositionAction === "WAIVE"
? ["不可变处置动作", "风险影响评估", "豁免理由"]
: ["不可变处置动作", "拟丢弃范围", "完整性影响评估"];
const metadata = addOperationsAudit({
type: "quality",
event: `${issue.title}提交质量复核`,
object: issue.id,
result: "待复核",
evidence,
});
operationsStore.qualityDispositions.push({
id: operationToken("QD"),
status: "PENDING_REVIEW",
actor: metadata.actor,
time: metadata.time,
trace: metadata.trace,
evidence: [...metadata.evidence],
issueId: issue.id,
action: dispositionAction,
reason,
reviewer,
immutable: true,
verification:
dispositionAction === "FIX_REPLAY"
? {
expectedRecords: 3860,
replayedRecords: null,
rulesPassed: false,
reviewerSigned: false,
}
: null,
});
migrateOperationsEntity(issue, "QUALITY_REVIEW_PENDING", metadata);
issue.timeline.push({
id: operationToken("STEP"),
status: "QUALITY_REVIEW_PENDING",
actor: metadata.actor,
time: metadata.time,
trace: metadata.trace,
evidence: [...metadata.evidence],
note: `${{ FIX_REPLAY: "修复并重放", WAIVE: "豁免", DISCARD: "丢弃" }[dispositionAction]}已提交 ${reviewer} 复核。`,
});
const workOrder = operationsStore.workOrders.find((item) => item.issueId === issue.id);
if (workOrder) migrateOperationsEntity(workOrder, "QUALITY_REVIEW_PENDING", metadata);
saveOperationsStore();
renderOperations();
renderAudit();
openIssueDrawer(issue.id);
showToast("处置动作已固化并写入审计,等待独立质量复核", "质量处置已提交");
return;
}
if (action === "verify-blocked" && issue.status === "QUALITY_REVIEW_PENDING") {
const disposition = latestQualityDisposition(issue.id);
if (!disposition || disposition.action === "FIX_REPLAY") return;
const actionLabel = disposition.action === "WAIVE" ? "风险豁免" : "丢弃异常数据";
const metadata = addOperationsAudit({
type: "quality",
event: `${issue.title}${actionLabel}复核完成`,
object: disposition.id,
actor: disposition.reviewer,
result: "REVIEWED_BLOCKED",
evidence: disposition.action === "WAIVE" ? ["风险影响评估", "独立复核签名", "需另行风险审批"] : ["丢弃范围清单", "完整性影响评估", "需重建候选并复核"],
});
migrateOperationsEntity(disposition, "REVIEWED_BLOCKED", metadata);
migrateOperationsEntity(issue, "INVESTIGATING", metadata);
issue.timeline.push({
id: operationToken("STEP"),
status: "REVIEWED_BLOCKED",
actor: metadata.actor,
time: metadata.time,
trace: metadata.trace,
evidence: [...metadata.evidence],
note:
disposition.action === "WAIVE"
? "风险豁免不等同于质量验证通过;候选门禁保持阻断,必须另行提交风险审批。"
: "丢弃可能改变数据完整性;候选门禁保持阻断,必须重建候选并重新执行质量验证。",
});
const workOrder = operationsStore.workOrders.find((item) => item.issueId === issue.id);
if (workOrder) migrateOperationsEntity(workOrder, "INVESTIGATING", metadata);
updateReleaseQualityGate();
saveOperationsStore();
renderOperations();
renderAudit();
openIssueDrawer(issue.id);
showToast(`${actionLabel}已复核,但不能解除发布门禁`, "质量门禁仍阻断");
return;
}
if (action === "verify" && issue.status === "QUALITY_REVIEW_PENDING") {
const disposition = latestQualityDisposition(issue.id);
if (!disposition || disposition.action !== "FIX_REPLAY") {
showToast("只有修复并重放可以进入质量验证通过状态", "质量门禁仍阻断");
return;
}
const metadata = addOperationsAudit({
type: "quality",
event: `${issue.title}质量复核通过`,
object: disposition.id,
actor: disposition.reviewer,
result: "VERIFIED",
evidence: ["重放结果 3,860/3,860", "质量规则全量通过", "复核签名"],
});
disposition.verification = {
expectedRecords: 3860,
replayedRecords: 3860,
rulesPassed: true,
reviewerSigned: true,
};
migrateOperationsEntity(disposition, "VERIFIED", metadata);
migrateOperationsEntity(issue, "VERIFIED", metadata);
issue.timeline.push({
id: operationToken("STEP"),
status: "VERIFIED",
actor: metadata.actor,
time: metadata.time,
trace: metadata.trace,
evidence: [...metadata.evidence],
note: "记录数、质量规则与重放证据已复核,候选版本质量门禁解除。",
});
const workOrder = operationsStore.workOrders.find((item) => item.issueId === issue.id);
if (workOrder) migrateOperationsEntity(workOrder, "VERIFIED", metadata);
updateReleaseQualityGate();
saveOperationsStore();
renderOperations();
renderAudit();
openIssueDrawer(issue.id);
showToast("质量处置已 VERIFIED,候选版本现在可提交审批", "质量门禁已通过");
return;
}
if (action === "resolve" && (issue.status === "VERIFIED" || (issue.status === "INVESTIGATING" && !issue.qualityRequired))) {
transitionIssue(issue, "RESOLVED", "影响已消除,稳定版本与后续任务验证正常。", ["恢复指标", "验证报告"]);
return;
}
if (action === "close" && issue.status === "RESOLVED") {
transitionIssue(issue, "CLOSED", "问题复盘完成,工单正式关闭。", ["关闭检查单", "复盘摘要"]);
}
});
document.getElementById("releaseWorkspace").addEventListener("click", (event) => {
const release = currentRelease();
const approval = currentApproval();
if (event.target.closest("[data-release-manifest]")) {
generateCandidateManifest();
return;
}
if (event.target.closest("[data-release-official-manifest]")) {
if (!release.publishedManifest) {
showToast("尚未形成发布 Manifest", "下载不可用");
return;
}
downloadJson(`${release.packageId}-published-manifest.json`, release.publishedManifest);
showToast(`${release.packageId} 发布 Manifest 已下载,审批引用为 ${release.publishedManifest.approval_reference}`, "发布 Manifest");
return;
}
if (event.target.closest("[data-release-submit]")) {
updateReleaseQualityGate();
if (!releaseReadyForApproval(release)) {
showToast(
release.qualityGate === "PASS" ? "质量门禁已通过,但必须先生成并冻结候选 Manifest" : "只有验证通过的 FIX_REPLAY 才能解除候选版本门禁",
"候选尚不可提交",
);
return;
}
const metadata = addOperationsAudit({
type: "publish",
event: "候选版本提交独立审批",
object: release.id,
result: "待审批",
evidence: ["候选 Manifest", "质量门禁 PASS", "影响租户清单"],
});
migrateOperationsEntity(release, "PENDING_APPROVAL", metadata);
migrateOperationsEntity(approval, "PENDING_APPROVAL", metadata);
saveOperationsStore();
renderOperations();
renderAudit();
showToast(`${release.id} 已提交,申请人不能批准自己的申请`, "审批单已创建");
return;
}
if (event.target.closest("[data-release-resubmit]")) {
const metadata = addOperationsAudit({
type: "publish",
event: "驳回候选返回草稿重建",
object: release.id,
result: "草稿",
evidence: ["审批驳回意见", "候选失效记录", "质量报告"],
});
migrateOperationsEntity(release, "DRAFT", metadata);
migrateOperationsEntity(approval, "DRAFT", metadata);
release.manifestGeneratedAt = null;
release.candidateManifest = null;
release.candidateInputFingerprint = null;
approval.approver = null;
approval.comment = "";
approval.decidedAt = null;
saveOperationsStore();
renderOperations();
renderAudit();
showToast("旧候选 Manifest 已失效,请完成修订后重新生成", "候选已返回草稿");
return;
}
if (event.target.closest("[data-release-approve]") || event.target.closest("[data-release-reject]")) {
if (release.status !== "PENDING_APPROVAL" || approval.status !== "PENDING_APPROVAL") return;
const approver = document.getElementById("releaseApprover")?.value;
const comment = document.getElementById("approvalComment")?.value.trim();
const approved = Boolean(event.target.closest("[data-release-approve]"));
decideIndependentApproval({
approval,
target: release,
approver,
comment,
approved,
approveStatus: "APPROVED",
rejectStatus: "REJECTED",
approveEvent: "候选版本独立审批通过",
rejectEvent: "候选版本独立审批驳回",
object: release.id,
evidence: ["审批意见", "审批人身份", "四眼规则校验"],
defaultApproveComment: "审批通过",
defaultRejectComment: "需修订后重提",
resultLabel: operationStatusLabel,
successMessage: (actor, passed) => `${actor} 已${passed ? "批准" : "驳回"} ${release.id}`,
successTitle: (passed) => (passed ? "独立审批通过" : "审批已驳回"),
});
return;
}
if (event.target.closest("[data-release-publish]") && release.status === "APPROVED") {
if (
approval.status !== "APPROVED" ||
!isIndependentApproval(approval) ||
!release.candidateManifest ||
!release.manifestGeneratedAt
) {
showToast("发布需要有效候选 Manifest 和已完成职责分离的独立审批", "发布前置校验失败");
return;
}
const metadata = addOperationsAudit({
type: "publish",
event: "候选版本发布为稳定版本",
object: `${release.id} · ${release.dataVersion}`,
result: "已发布",
evidence: [`独立审批单 ${approval.id}:APPROVED`, "候选 Manifest", "稳定别名切换记录"],
});
migrateOperationsEntity(release, "PUBLISHED", metadata);
release.publishedAt = metadata.time;
release.publishedManifest = buildPackageManifest({ releaseStatus: "PUBLISHED", approvalStatus: "APPROVED" });
release.publishedManifestGeneratedAt = release.publishedManifest.generated_at;
saveOperationsStore();
downloadJson(`${release.packageId}-published-manifest.json`, release.publishedManifest);
state.operationsTab = "deliveries";
renderOperations();
renderAudit();
showToast(`稳定别名已切换,发布 Manifest 引用 ${approval.id}:APPROVED;尚未向学校发送`, "候选版本已发布");
return;
}
if (event.target.closest("[data-rollback-submit]") && release.status === "PUBLISHED") {
const rollbackApproval = currentRollbackApproval();
const reason = document.getElementById("rollbackReason")?.value.trim();
if (!reason) {
showToast("必须填写回滚原因与生产影响", "缺少回滚依据");
return;
}
const metadata = addOperationsAudit({
type: "publish",
event: "全局版本回滚提交独立审批",
object: `${release.id} → ${release.previousStableVersion}`,
result: "ROLLBACK_PENDING_APPROVAL",
evidence: ["回滚原因", "影响租户清单", "目标稳定版本"],
});
rollbackApproval.reason = reason;
rollbackApproval.approver = null;
rollbackApproval.comment = "";
rollbackApproval.decidedAt = null;
migrateOperationsEntity(rollbackApproval, "ROLLBACK_PENDING_APPROVAL", metadata);
saveOperationsStore();
renderOperations();
renderAudit();
showToast("全局回滚申请已提交,申请人不能批准自己的回滚", "等待独立回滚审批");
return;
}
if (event.target.closest("[data-rollback-approve]") || event.target.closest("[data-rollback-reject]")) {
const rollbackApproval = currentRollbackApproval();
if (release.status !== "PUBLISHED" || rollbackApproval?.status !== "ROLLBACK_PENDING_APPROVAL") return;
const approver = document.getElementById("rollbackApprover")?.value;
const comment = document.getElementById("rollbackApprovalComment")?.value.trim();
const approved = Boolean(event.target.closest("[data-rollback-approve]"));
decideIndependentApproval({
approval: rollbackApproval,
approver,
comment,
approved,
approveStatus: "ROLLBACK_APPROVED",
rejectStatus: "ROLLBACK_REJECTED",
approveEvent: "全局版本回滚独立审批通过",
rejectEvent: "全局版本回滚独立审批驳回",
object: `${release.id} → ${release.previousStableVersion}`,
evidence: ["回滚审批意见", "审批人身份", "四眼规则校验", "影响租户清单"],
defaultApproveComment: "同意在受控窗口执行回滚",
defaultRejectComment: "回滚依据不足",
successMessage: (actor, passed) => `${actor} 已${passed ? "批准" : "驳回"}全局回滚`,
successTitle: (passed) => (passed ? "回滚审批通过" : "回滚审批驳回"),
});
return;
}
if (event.target.closest("[data-release-rollback]") && release.status === "PUBLISHED") {
const rollbackApproval = currentRollbackApproval();
if (
rollbackApproval?.status !== "ROLLBACK_APPROVED" ||
!isIndependentApproval(rollbackApproval)
) {
showToast("全局版本回滚必须先完成职责分离的独立审批", "回滚执行被阻断");
return;
}
const metadata = addOperationsAudit({
type: "publish",
event: "发布版本全局回滚",
object: release.id,
result: "已回滚",
evidence: [`稳定别名恢复 ${release.previousStableVersion}`, `回滚审批 ${rollbackApproval.id}:${rollbackApproval.status}`, "影响范围锁"],
});
migrateOperationsEntity(release, "ROLLED_BACK", metadata);
migrateOperationsEntity(rollbackApproval, "ROLLBACK_EXECUTED", metadata);
saveOperationsStore();
renderOperations();
renderAudit();
showToast(`稳定别名已恢复为 ${release.previousStableVersion}`, "版本已回滚");
}
});
document.getElementById("startPendingDeliveries").addEventListener("click", () => {
if (currentRelease()?.status !== "PUBLISHED") {
showToast("只有已发布版本才能启动学校交付", "交付尚不可用");
return;
}
const pending = operationsStore.deliveries.filter((delivery) => delivery.status === "PENDING");
pending.forEach((delivery) => {
const metadata = addOperationsAudit({
type: "publish",
event: "租户交付进入发送队列",
object: `${delivery.tenantId} · ${delivery.packageId}`,
actor: "delivery-orchestrator",
result: "SENDING",
evidence: ["租户范围校验", "包签名", "幂等键"],
});
migrateOperationsEntity(delivery, "SENDING", metadata);
delivery.attempts += 1;
});
saveOperationsStore();
renderOperations();
renderAudit();
showToast(`${pending.length} 所待发送学校已进入原型队列,未发生真实网络发送`, "交付队列已启动");
});
document.getElementById("deliveryTable").addEventListener("click", (event) => {
const button =
event.target.closest("[data-delivery-delivered]") ||
event.target.closest("[data-delivery-failed]") ||
event.target.closest("[data-delivery-retry]") ||
event.target.closest("[data-delivery-ack]") ||
event.target.closest("[data-delivery-rollback]");
if (!button) return;
const id =
button.dataset.deliveryDelivered ||
button.dataset.deliveryFailed ||
button.dataset.deliveryRetry ||
button.dataset.deliveryAck ||
button.dataset.deliveryRollback;
const delivery = operationsStore.deliveries.find((item) => item.id === id);
if (!delivery) return;
if (button.dataset.deliveryDelivered) {
const metadata = addOperationsAudit({
type: "publish",
event: "租户交付包模拟送达",
object: delivery.tenantId,
actor: "delivery-orchestrator",
result: "DELIVERED",
evidence: ["传输完成标记", "包签名"],
});
migrateOperationsEntity(delivery, "DELIVERED", metadata);
delivery.failureReason = "";
} else if (button.dataset.deliveryFailed) {
const metadata = addOperationsAudit({
type: "publish",
event: "租户交付失败隔离",
object: delivery.tenantId,
actor: "delivery-orchestrator",
result: "FAILED",
evidence: ["模拟超时日志", "租户隔离策略"],
});
migrateOperationsEntity(delivery, "FAILED", metadata);
delivery.failureReason = "模拟租户端接收超时,其他学校未受影响";
} else if (button.dataset.deliveryRetry) {
const metadata = addOperationsAudit({
type: "publish",
event: "失败租户单独重试",
object: delivery.tenantId,
actor: OPERATIONS_ACTOR,
result: "RETRYING",
evidence: ["原包幂等键", "失败原因", "单租户重试指令"],
});
migrateOperationsEntity(delivery, "RETRYING", metadata);
delivery.attempts += 1;
delivery.failureReason = "";
} else if (button.dataset.deliveryAck) {
const countInput = document.getElementById(`receipt-count-${delivery.id}`);
const shaInput = document.getElementById(`receipt-sha-${delivery.id}`);
const receiptRecords = Number(countInput?.value);
const receiptSha = shaInput?.value.trim();
if (receiptRecords !== delivery.expectedRecords || receiptSha !== delivery.expectedSha) {
showToast("记录数与 SHA-256 必须同时等于候选包期望值,当前回执未确认", "回执校验失败");
return;
}
const metadata = addOperationsAudit({
type: "publish",
event: "学校回执校验通过",
object: delivery.tenantId,
actor: "receipt-verifier",
result: "ACKNOWLEDGED",
evidence: [`记录数 ${receiptRecords}`, `SHA-256 ${receiptSha}`, "回执签名"],
});
migrateOperationsEntity(delivery, "ACKNOWLEDGED", metadata);
delivery.receiptRecords = receiptRecords;
delivery.receiptSha = receiptSha;
} else if (button.dataset.deliveryRollback) {
const metadata = addOperationsAudit({
type: "publish",
event: "单租户交付回滚",
object: `${delivery.tenantId} · ${delivery.dataVersion}`,
actor: OPERATIONS_ACTOR,
result: "ROLLED_BACK",
evidence: ["租户范围锁", `恢复 ${currentRelease().previousStableVersion}`, "回滚记录"],
});
migrateOperationsEntity(delivery, "ROLLED_BACK", metadata);
}
saveOperationsStore();
renderOperations();
renderAudit();
showToast(`${delivery.school}:${operationStatusLabel(delivery.status)}`, "学校交付状态已更新");
});
}
function setupGlobalActions() {
document.querySelectorAll("[data-close-modal]").forEach((button) => button.addEventListener("click", () => closeModal(button.closest(".modal-backdrop"))));
document.querySelectorAll(".modal-backdrop").forEach((backdrop) => {
backdrop.addEventListener("click", (event) => {
if (event.target === backdrop) closeModal(backdrop);
});
});
document.getElementById("notificationButton").addEventListener("click", () => {
const openIssues = operationsStore.issues.filter((issue) => !["RESOLVED", "CLOSED"].includes(issue.status));
const pendingApproval = operationsStore.approvals.find((approval) => ["PENDING_APPROVAL", "ROLLBACK_PENDING_APPROVAL"].includes(approval.status));
const deliveryTodos = operationsStore.deliveries.filter((delivery) => ["PENDING", "SENDING", "FAILED", "RETRYING"].includes(delivery.status));
openInfoModal(
"OPERATIONS NOTIFICATIONS",
"真实运营待办",
`<div class="runtime-list notification-list">
${openIssues
.map(
(issue) =>
`<button data-open-issue="${h(issue.id)}"><strong>${h(issue.title)}</strong><small>${h(operationStatusLabel(issue.status))} · ${h(issue.owner)} · ${h(issue.sla)}</small></button>`,
)
.join("")}
${
pendingApproval
? `<button data-operations-jump="releases"><strong>${h(pendingApproval.releaseId)} ${pendingApproval.type === "ROLLBACK" ? "等待独立回滚审批" : "等待独立发布审批"}</strong><small>申请人 ${h(pendingApproval.applicant)} 不能批准自己的申请</small></button>`
: ""
}
${deliveryTodos.length ? `<button data-operations-jump="deliveries"><strong>${deliveryTodos.length} 所学校交付待处理</strong><small>包含待发送、发送中、失败或重试状态</small></button>` : ""}
${!openIssues.length && !pendingApproval && !deliveryTodos.length ? `<span><strong>当前没有运营待办</strong><small>问题、审批和交付均已闭环</small></span>` : ""}
</div>`,
);
});
document.getElementById("operatorButton").addEventListener("click", () => {
openInfoModal(
"OPERATOR PROFILE",
"厂商数据运营管理员",
`<div class="info-grid"><span>姓名<strong>陈致远</strong></span><span>角色<strong>数据运营管理员</strong></span><span>权限范围<strong>全国招聘数据域</strong></span><span>最近登录<strong>今天 08:12</strong></span></div><div class="info-note">可管理数据源、采集任务、标准契约、质量规则和数据交付;生产发布需要数据产品负责人复核。</div>`,
);
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
if (document.getElementById("issueDrawer").classList.contains("open")) {
closeIssueDrawer();
return;
}
if (document.getElementById("recordDrawer").classList.contains("open")) {
closeRecordDrawer();
return;
}
if (document.getElementById("sourceDrawer").classList.contains("open")) {
closeSourceDrawer();
return;
}
const modal = document.querySelector(".modal-backdrop.open");
if (modal) {
closeModal(modal);
return;
}
document.body.classList.remove("sidebar-open");
});
}
function setupOverviewActions() {
document.querySelector("[data-open-export]").addEventListener("click", exportDailyReport);
}
function populateJobSources() {
document.getElementById("jobSource").innerHTML = sources.map((source) => `<option value="${h(source.id)}">${h(source.name)} · ${h(source.categoryLabel)}</option>`).join("");
}
function init() {
renderOverviewSources();
renderSources();
renderCollectionJobs();
populateRecordSources();
renderRecords();
renderContract();
renderExchangeLogs();
renderQuality();
renderOperations();
renderAudit();
populateJobSources();
setupNavigation();
setupSourceRegistry();
setupSourceDrawer();
setupCollectionJobs();
setupRecords();
setupContracts();
setupExchange();
setupQuality();
setupOperations();
setupAudit();
setupGlobalActions();
setupOverviewActions();
}
document.addEventListener("DOMContentLoaded", init);