Преглед изворни кода

分数线1-15志愿数据处理

chengjie пре 1 недеља
родитељ
комит
90a3770cb4
42 измењених фајлова са 64999 додато и 141081 уклоњено
  1. 159 0
      秒过分数线数据导入/2026_1-15志愿导入经验说明.md
  2. 392 0
      秒过分数线数据导入/audit_mps_score_1_15_2026_final.js
  3. 265 0
      秒过分数线数据导入/fix_mps_score_1_15_2026_missing_plan_rows.js
  4. 421 0
      秒过分数线数据导入/generate_mps_score_1_15_2026_full_review_report.js
  5. 389 0
      秒过分数线数据导入/import_mps_score_1_15_baoshan_scores_2026.js
  6. 382 0
      秒过分数线数据导入/import_mps_score_1_15_batch_from_json_2026.js
  7. 435 0
      秒过分数线数据导入/import_mps_score_1_15_pudong_scores_2026.js
  8. 378 0
      秒过分数线数据导入/import_mps_score_1_15_qingpu_scores_2026.js
  9. 396 0
      秒过分数线数据导入/import_mps_score_1_15_xuhui_scores_2026.js
  10. 2996 0
      秒过分数线数据导入/mps_score_1_15_2026_full_review_report.json
  11. 78 0
      秒过分数线数据导入/mps_score_1_15_2026_full_review_report.md
  12. 180 0
      秒过分数线数据导入/mps_score_1_15_2026_plan_crosscheck_report.json
  13. 24 0
      秒过分数线数据导入/mps_score_1_15_2026_plan_crosscheck_report.md
  14. 4000 0
      秒过分数线数据导入/mps_score_1_15_2026_sample_audit_report.json
  15. 93 0
      秒过分数线数据导入/mps_score_1_15_2026_sample_audit_report.md
  16. 7988 0
      秒过分数线数据导入/mps_score_1_15_all_extracted_sources_2026.json
  17. 1145 0
      秒过分数线数据导入/mps_score_1_15_baoshan_scores_2026_import_report.json
  18. 5084 0
      秒过分数线数据导入/mps_score_1_15_batch_extracted_2026.json
  19. 15722 0
      秒过分数线数据导入/mps_score_1_15_batch_import_report_2026.json
  20. 68 0
      秒过分数线数据导入/mps_score_1_15_changning_image_extracted_2026.json
  21. 61 0
      秒过分数线数据导入/mps_score_1_15_hongkou_image_extracted_2026.json
  22. 78 0
      秒过分数线数据导入/mps_score_1_15_huangpu_image_extracted_2026.json
  23. 40 0
      秒过分数线数据导入/mps_score_1_15_image_extracted_2026.json
  24. 91 0
      秒过分数线数据导入/mps_score_1_15_minhang_image_extracted_2026.json
  25. 1131 0
      秒过分数线数据导入/mps_score_1_15_minhang_pdf_extracted_2026.json
  26. 1725 0
      秒过分数线数据导入/mps_score_1_15_minhang_pdf_verify_report_2026.json
  27. 2170 0
      秒过分数线数据导入/mps_score_1_15_new_source_verify_2026.json
  28. 3780 0
      秒过分数线数据导入/mps_score_1_15_new_source_verify_report_2026.json
  29. 1940 0
      秒过分数线数据导入/mps_score_1_15_pudong_scores_2026_import_report.json
  30. 605 0
      秒过分数线数据导入/mps_score_1_15_qingpu_html_extracted_2026.json
  31. 1179 0
      秒过分数线数据导入/mps_score_1_15_qingpu_html_verify_report_2026.json
  32. 991 0
      秒过分数线数据导入/mps_score_1_15_qingpu_scores_2026_import_report.json
  33. 9317 0
      秒过分数线数据导入/mps_score_1_15_verified_generic_sources_2026.json
  34. 1242 0
      秒过分数线数据导入/mps_score_1_15_xuhui_scores_2026_import_report.json
  35. 0 4592
      秒过分数线数据导入/mps_score_huangpu_scores_2026_backup.json
  36. 0 104
      秒过分数线数据导入/mps_score_qingpu_yizhong_shangmei_2026_backup.json
  37. 0 52814
      秒过分数线数据导入/mps_score_school_quota_2026_bad_targets_backup.json
  38. 0 2361
      秒过分数线数据导入/mps_score_school_quota_2026_qingpu_cross_page_fix_backup.json
  39. 0 2360
      秒过分数线数据导入/mps_score_school_quota_2026_qingpu_reparse_backup.json
  40. 0 78746
      秒过分数线数据导入/mps_score_scores_2026_backup.json
  41. 0 104
      秒过分数线数据导入/mps_score_scores_2026_tie_preferred_backup.json
  42. 54 0
      秒过分数线数据导入/ocr_image_text.swift

+ 159 - 0
秒过分数线数据导入/2026_1-15志愿导入经验说明.md

@@ -0,0 +1,159 @@
1
+# 2026 年 1-15 志愿分数线导入经验说明
2
+
3
+本文用于明年继续处理上海中考 `1-15志愿` 分数线时快速上手。
4
+
5
+## 目标和表
6
+
7
+- 数据库表:`kylx365_db.MPS_Score`
8
+- 年份:`ScoreYear = 2026`
9
+- 类型:`ScoreType = 1-15志愿`
10
+- 关键字段:`DistrictID`、`SchoolTarget`、`SchoolFullName`、`SchoolTargetRemark`、`PlanNum`、`ScoreTotal`、`Score1`、`Score2`、`Score3`、`Score4`、`ScoreTotalDifferenceValue`
11
+- 2026 最终库内总量:16 个区,961 行,重复 key 为 0
12
+
13
+## 目录
14
+
15
+源文件目录:
16
+
17
+```text
18
+/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿
19
+```
20
+
21
+计划文件目录:
22
+
23
+```text
24
+/Volumes/程杰外接SD盘/上海中考招生计划/2026/计划/1-15志愿
25
+```
26
+
27
+工作目录:
28
+
29
+```text
30
+/Users/chengjie/Documents/git/miaoguo_system_server/秒过分数线数据导入
31
+```
32
+
33
+## 主要脚本
34
+
35
+- `import_mps_score_1_15_batch_from_json_2026.js`
36
+  - 通用 JSON 导入器,按学校名和备注匹配数据库计划行。
37
+  - 适合文字层 PDF、HTML、MHTML、已人工/OCR 整理后的 JSON。
38
+- `import_mps_score_1_15_xuhui_scores_2026.js`
39
+  - 徐汇专用,因源 PDF 是扫描图,最终按 3 页 PDF 手工核准。
40
+- `import_mps_score_1_15_baoshan_scores_2026.js`
41
+  - 宝山专用,处理大量总分-only 行。
42
+- `import_mps_score_1_15_pudong_scores_2026.js`
43
+  - 浦东专用,处理多页大表和 1 条库有源无计划行。
44
+- `audit_mps_score_1_15_2026_final.js`
45
+  - 最终抽样审计脚本:比对源报告和数据库、查重复 key、生成高风险抽样清单。
46
+- `generate_mps_score_1_15_2026_full_review_report.js`
47
+  - 汇总数据库、最新干跑报告、计划交叉核查报告,重写全量复查报告。
48
+- `fix_mps_score_1_15_2026_missing_plan_rows.js`
49
+  - 专门处理“分数线有、计划图也有、数据库缺计划行”的补库脚本;2026 已补松江区上海创艺高级中学 1 行。
50
+- `ocr_image_text.swift`
51
+  - macOS Vision OCR 辅助脚本,用于截图/JPEG/扫描 PDF 的文字识别。
52
+
53
+## 2026 源文件处理方式
54
+
55
+|区|最终依据|说明|
56
+|---|---|---|
57
+|黄浦|`黄浦区.pdf`|文字层 PDF,67 行;PDF 无综合测试列,`Score4` 保持空。|
58
+|徐汇|`徐汇区.pdf`|扫描 PDF,3 页;曾漏第 3 页,已补齐 68 行。|
59
+|长宁|`长宁区.pdf`|文字层 PDF,57 行;库内另有新纪元计划行,源 PDF 未列,保留 0 分。|
60
+|静安|`静安区-本区.pdf` + `静安区-外区.pdf`|源有宋庆龄、博华、新纪元 3 行库无;已查计划图 `静安区2.png` 未见,不补库,列入源/计划不一致复查。库有新和停招,按停招保留。|
61
+|普陀|`普陀区.pdf`|文字层 PDF,68 行。|
62
+|虹口|`虹口区.mhtml`|MHTML 表格,使用简称映射到数据库全称,50 行。|
63
+|杨浦|`杨浦区.html`|源有 `复旦大学第二附属中学`,已查 `杨浦区1.png` + `杨浦区2.png` 未见计划,不补库,列入源/计划不一致复查。|
64
+|闵行|`闵行区.pdf`|文字层 PDF,80 行;5 行只公布总分。|
65
+|宝山|`宝山区.pdf`|PDF 2 页,59 行;17 行只公布总分。|
66
+|嘉定|`嘉定区.pdf`|文字层 PDF,54 行;33 行只公布总分。|
67
+|浦东|`浦东新区.pdf`|PDF 3 页,106 源行;库内另有川沙中学北校,源未列,保留 0 分。|
68
+|金山|`金山区.pdf`|源 40 行,库 42 行;2 条库有源无,列入复查。|
69
+|松江|`松江区.mhtml`|源 58 行,匹配 56;计划图确认上海创艺高级中学有 2 个计划,已补入数据库 ID 40207。宝山世外、远东在计划图未见,不补库;5 条库有源无按未投档保留。|
70
+|青浦|`青浦区.html`|HTML 54 行;当前 HTML 已复核,changed_rows=0。|
71
+|奉贤|`奉贤区.html`|HTML 46 行。|
72
+|崇明|`崇明区1.jpeg`|JPEG 单图,29 行;低分段已人工视觉确认。|
73
+
74
+## 数据约定
75
+
76
+- `ScoreTotal` 为录取最低分。
77
+- `Score1` 为语数外总分。
78
+- `Score2` 为数学。
79
+- `Score3` 为语文。
80
+- `Score4` 为综合测试。
81
+- 源文件只有总分、没有末位考生分项时:录入 `ScoreTotal`,`Score1` 到 `Score4` 留空。
82
+- 源文件为 `/`、无考生达线、线上无人投档、控分线上无考生录取时:录入 `ScoreTotal = 0`,分项通常为 0。
83
+- PDF 没有综合测试列时不要补猜,例如黄浦区 `Score4` 留空。
84
+- `ScoreTotalDifferenceValue` 用当前总分减上一年同 key 总分;无考生/0 分行建议保持空,避免出现误导性的负差。
85
+- 有计划但分数线源未列,是正常未投档/无学生报入,库内计划行保留 0 分,不算错误。
86
+- 分数线源有但数据库没有计划行时,必须查计划图:计划图有才补库,计划图未见则列为源文件/计划文件不一致复查,不要强行补到数据库。
87
+
88
+## 推荐流程
89
+
90
+1. 先确认当前库里 2026 年计划行是否已建好:
91
+
92
+```sql
93
+SELECT DistrictID, COUNT(*), SUM(PlanNum)
94
+FROM MPS_Score
95
+WHERE ScoreYear = 2026 AND ScoreType = '1-15志愿'
96
+GROUP BY DistrictID
97
+ORDER BY DistrictID;
98
+```
99
+
100
+2. 对源文件分类:
101
+   - 文字层 PDF:优先用 `pdfplumber.extract_tables()`。
102
+   - HTML/MHTML:用 HTML parser 抽 `<table>`。
103
+   - 扫描 PDF/JPEG/PNG:先 `pdftoppm` 渲染,再用 Vision OCR 和人工视觉核对。
104
+3. 抽成统一 JSON,字段用 `schoolName`、`scoreTotal`、`score1`、`score2`、`score3`、`score4`、`officialRemark`。
105
+4. 先干跑:
106
+
107
+```bash
108
+node import_mps_score_1_15_batch_from_json_2026.js --input=输入.json --dry-run
109
+```
110
+
111
+5. 确认 `problem_rows` 和 `not_in_source_rows`:
112
+   - `not_in_source_rows`:库有计划、分数线源未列,通常是未投档,保留。
113
+   - `problem_rows` / `missing_current_row`:先查计划目录。计划图有则补库,计划图未见则记报告。
114
+
115
+6. 正式导入:
116
+
117
+```bash
118
+node import_mps_score_1_15_batch_from_json_2026.js --input=输入.json
119
+```
120
+
121
+7. 跑最终审计和全量报告:
122
+
123
+```bash
124
+node generate_mps_score_1_15_2026_full_review_report.js
125
+node audit_mps_score_1_15_2026_final.js
126
+node generate_mps_score_1_15_2026_full_review_report.js
127
+```
128
+
129
+## 常见坑
130
+
131
+- 官方源可能后续更新格式,目录里后来的 PDF/HTML 要优先于早期截图。
132
+- 学校名带括号时,数据库可能拆在 `SchoolTargetRemark`,例如艺术班、英语等。
133
+- 虹口 MHTML 使用简称,要先映射到数据库全称。
134
+- 静安、杨浦、松江等可能出现源文件有学校但计划库无对应行,不能强行写到别的学校;先用计划图核实。
135
+- 低分段常被误判为 OCR 错误,必须看源图:2026 年宝山民一综合 `59`、崇明民一数学 `40` 都是真值。
136
+- `/` 和 `未录满` 不总是等于 0;有些区会写总分 `501` 且无分项,这应作为总分-only,而不是 0。
137
+
138
+## 2026 已知待复查清单
139
+
140
+最终详细清单见:
141
+
142
+- `mps_score_1_15_2026_full_review_report.json`
143
+- `mps_score_1_15_2026_full_review_report.md`
144
+- `mps_score_1_15_2026_sample_audit_report.json`
145
+- `mps_score_1_15_2026_sample_audit_report.md`
146
+
147
+重点仍是源文件和计划文件不一致。库有源无按未投档保留,不再作为错误:
148
+
149
+- 静安:源有宋庆龄、博华、新纪元,计划图未见,库无。
150
+- 杨浦:源有 `复旦大学第二附属中学`,计划图未见,库无。
151
+- 松江:源有宝山世外、远东,计划图未见,库无。
152
+- 松江:源有创艺且计划图有 2 个计划,已补入库,不再是问题。
153
+- 长宁、浦东、金山、松江的库有源无行:有计划但分数线未列,按未投档保留 0 分。
154
+
155
+## 清理原则
156
+
157
+- 保留导入脚本、最终源 JSON、最终审计报告和本经验说明。
158
+- 可删除 `*backup*.json`、`tmp/`、`tmp_*.png`、`tmp_*.json`、`__pycache__/`。
159
+- 清理前先确认不再需要回滚数据库;备份文件只用于当次导入回滚。

+ 392 - 0
秒过分数线数据导入/audit_mps_score_1_15_2026_final.js

@@ -0,0 +1,392 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const SCORE_TYPE = "1-15志愿";
11
+
12
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
+const JSON_REPORT = path.join(SCRIPT_DIR, "mps_score_1_15_2026_sample_audit_report.json");
14
+const MD_REPORT = path.join(SCRIPT_DIR, "mps_score_1_15_2026_sample_audit_report.md");
15
+
16
+const DISTRICT_NAMES = new Map([
17
+  [1, "黄浦区"],
18
+  [2, "徐汇区"],
19
+  [3, "长宁区"],
20
+  [4, "静安区"],
21
+  [5, "普陀区"],
22
+  [6, "虹口区"],
23
+  [7, "杨浦区"],
24
+  [8, "闵行区"],
25
+  [9, "宝山区"],
26
+  [10, "嘉定区"],
27
+  [11, "浦东新区"],
28
+  [12, "金山区"],
29
+  [13, "松江区"],
30
+  [14, "青浦区"],
31
+  [15, "奉贤区"],
32
+  [16, "崇明区"],
33
+]);
34
+
35
+const HIGH_RISK_DISTRICTS = new Set([2, 9, 11, 14, 16]);
36
+const SCORE_COLUMNS = ["ScoreTotal", "Score1", "Score2", "Score3", "Score4"];
37
+
38
+const reportFiles = [
39
+  "mps_score_1_15_batch_import_report_2026.json",
40
+  "mps_score_1_15_xuhui_scores_2026_import_report.json",
41
+  "mps_score_1_15_baoshan_scores_2026_import_report.json",
42
+  "mps_score_1_15_pudong_scores_2026_import_report.json",
43
+];
44
+
45
+const MANUAL_VISUAL_CHECKS = [
46
+  {
47
+    source: "宝山区.pdf page 2",
48
+    result: "确认上海民办民一中学为 483 / 311 / 70 / 110 / 59,综合测试 59 不是水印误读。",
49
+  },
50
+  {
51
+    source: "崇明区1.jpeg",
52
+    result: "确认上海民办民一中学为 441.5 / 228 / 40 / 100 / 82,上海新纪元双语学校为 464.5 / 236.5 / 67 / 98 / 94。",
53
+  },
54
+  {
55
+    source: "浦东新区.pdf page 2-3",
56
+    result: "确认低分段和未录满行,包括东鼎外国语 480.5 / 262.5 / 57 / 105 / 92,民一中学 483.5 / 242.5 / 74 / 117 / 105,新纪元 480.5 / 259.5 / 92 / 122 / 85。",
57
+  },
58
+  {
59
+    source: "徐汇区.pdf page 3",
60
+    result: "确认补录第 3 页 23 行,重点核对浦实 620.5、东鼎 576.5、励滕线上无人投档、民一 472、新纪元 517.5。",
61
+  },
62
+  {
63
+    source: "青浦区.html",
64
+    result: "确认当前 HTML 54 行全匹配数据库,宏润博源为 493.5,不是 0 分;正式核查 changed_rows=0。",
65
+  },
66
+];
67
+
68
+const n = (value) => {
69
+  if (value === null || value === undefined || value === "") {
70
+    return null;
71
+  }
72
+  const number = Number(value);
73
+  return Number.isFinite(number) ? number : null;
74
+};
75
+
76
+const fixed = (value) => {
77
+  const number = n(value);
78
+  return number === null ? null : Number(number.toFixed(2));
79
+};
80
+
81
+const hasHalf = (value) => {
82
+  const number = n(value);
83
+  return number !== null && Math.abs(number * 2 - Math.round(number * 2)) < 0.0001 && Math.abs(number - Math.round(number)) > 0.0001;
84
+};
85
+
86
+function deterministicHash(value) {
87
+  let hash = 0;
88
+  for (const char of String(value)) {
89
+    hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
90
+  }
91
+  return hash;
92
+}
93
+
94
+async function readJson(file) {
95
+  return JSON.parse(await fs.readFile(path.join(SCRIPT_DIR, file), "utf8"));
96
+}
97
+
98
+function collectExpectedRows(importReports) {
99
+  const rows = [];
100
+  for (const { file, report } of importReports) {
101
+    const districts = report.districts ?? [{ district: report.summary?.district, updates: report.updates ?? [] }];
102
+    for (const district of districts) {
103
+      for (const update of district.updates ?? []) {
104
+        rows.push({
105
+          sourceReport: file,
106
+          districtId: Number(update.districtId ?? district.district_id ?? report.summary?.district_id ?? update.DistrictID),
107
+          district: district.district ?? report.summary?.district,
108
+          id: Number(update.id),
109
+          schoolTarget: String(update.schoolTarget ?? ""),
110
+          schoolName: update.schoolName,
111
+          source: update.source ?? district.source ?? report.source_pdf ?? report.source_image ?? report.source_images ?? file,
112
+          page: update.page ?? null,
113
+          row: update.row ?? null,
114
+          officialCode: update.officialCode ?? "",
115
+          officialRemark: update.officialRemark ?? "",
116
+          ScoreTotal: fixed(update.ScoreTotal),
117
+          Score1: fixed(update.Score1),
118
+          Score2: fixed(update.Score2),
119
+          Score3: fixed(update.Score3),
120
+          Score4: fixed(update.Score4),
121
+        });
122
+      }
123
+    }
124
+  }
125
+  return rows;
126
+}
127
+
128
+function compareExpectedToDb(expectedRows, dbById) {
129
+  const mismatches = [];
130
+  for (const expected of expectedRows) {
131
+    const dbRow = dbById.get(expected.id);
132
+    if (!dbRow) {
133
+      mismatches.push({ type: "missing_db_row", expected });
134
+      continue;
135
+    }
136
+    for (const column of SCORE_COLUMNS) {
137
+      const expectedValue = expected[column];
138
+      const actualValue = fixed(dbRow[column]);
139
+      if (expectedValue !== actualValue) {
140
+        mismatches.push({
141
+          type: "value_mismatch",
142
+          id: expected.id,
143
+          districtId: expected.districtId,
144
+          district: DISTRICT_NAMES.get(expected.districtId),
145
+          schoolTarget: dbRow.SchoolTarget,
146
+          schoolName: dbRow.SchoolFullName,
147
+          column,
148
+          expected: expectedValue,
149
+          actual: actualValue,
150
+          source: expected.source,
151
+          page: expected.page,
152
+          row: expected.row,
153
+        });
154
+      }
155
+    }
156
+  }
157
+  return mismatches;
158
+}
159
+
160
+function collectRangeAnomalies(dbRows) {
161
+  const anomalies = [];
162
+  for (const row of dbRows) {
163
+    const scoreTotal = n(row.ScoreTotal);
164
+    const score1 = n(row.Score1);
165
+    const score2 = n(row.Score2);
166
+    const score3 = n(row.Score3);
167
+    const score4 = n(row.Score4);
168
+    const base = {
169
+      DistrictID: row.DistrictID,
170
+      DistrictName: DISTRICT_NAMES.get(Number(row.DistrictID)),
171
+      SchoolTarget: row.SchoolTarget,
172
+      SchoolFullName: row.SchoolFullName,
173
+      ScoreTotal: scoreTotal,
174
+      Score1: score1,
175
+      Score2: score2,
176
+      Score3: score3,
177
+      Score4: score4,
178
+    };
179
+    if (scoreTotal !== null && scoreTotal > 0 && (scoreTotal < 430 || scoreTotal > 725)) {
180
+      anomalies.push({ type: "score_total_outside_expected_range", ...base });
181
+    }
182
+    if (scoreTotal === null || scoreTotal === 0) {
183
+      continue;
184
+    }
185
+    if (score1 !== null && (score1 < 240 || score1 > 430 || (scoreTotal !== null && score1 > scoreTotal))) {
186
+      anomalies.push({ type: "score1_suspicious", ...base });
187
+    }
188
+    if (score2 !== null && (score2 < 40 || score2 > 150 || (score1 !== null && score2 > score1))) {
189
+      anomalies.push({ type: "score2_suspicious", ...base });
190
+    }
191
+    if (score3 !== null && (score3 < 70 || score3 > 150 || (score1 !== null && score3 > score1))) {
192
+      anomalies.push({ type: "score3_suspicious", ...base });
193
+    }
194
+    if (score4 !== null && (score4 < 60 || score4 > 150)) {
195
+      anomalies.push({ type: "score4_suspicious", ...base });
196
+    }
197
+    for (const column of SCORE_COLUMNS) {
198
+      const value = n(row[column]);
199
+      if (value !== null && Math.abs(value * 2 - Math.round(value * 2)) > 0.0001) {
200
+        anomalies.push({ type: "non_half_point_value", column, ...base });
201
+      }
202
+    }
203
+  }
204
+  return anomalies;
205
+}
206
+
207
+function selectSamples(expectedRows, dbById) {
208
+  const byDistrict = new Map();
209
+  for (const row of expectedRows) {
210
+    if (!byDistrict.has(row.districtId)) {
211
+      byDistrict.set(row.districtId, []);
212
+    }
213
+    byDistrict.get(row.districtId).push(row);
214
+  }
215
+
216
+  const samples = [];
217
+  for (const [districtId, rows] of [...byDistrict.entries()].sort((a, b) => a[0] - b[0])) {
218
+    const scored = rows.filter((row) => n(row.ScoreTotal) > 0).sort((a, b) => n(a.ScoreTotal) - n(b.ScoreTotal));
219
+    const totalOnly = rows.filter((row) => n(row.ScoreTotal) > 0 && row.Score1 === null);
220
+    const zeroRows = rows.filter((row) => n(row.ScoreTotal) === 0);
221
+    const halfRows = rows.filter((row) => hasHalf(row.ScoreTotal) || hasHalf(row.Score1));
222
+    const buckets = [
223
+      { reason: "min_score", row: scored[0] },
224
+      { reason: "median_score", row: scored[Math.floor(scored.length / 2)] },
225
+      { reason: "max_score", row: scored[scored.length - 1] },
226
+      ...totalOnly.slice(0, HIGH_RISK_DISTRICTS.has(districtId) ? 6 : 2).map((row) => ({ reason: "total_only", row })),
227
+      ...zeroRows.slice(0, HIGH_RISK_DISTRICTS.has(districtId) ? 6 : 2).map((row) => ({ reason: "zero_or_no_admission", row })),
228
+      ...halfRows
229
+        .sort((a, b) => deterministicHash(`${a.id}:${a.schoolName}`) - deterministicHash(`${b.id}:${b.schoolName}`))
230
+        .slice(0, HIGH_RISK_DISTRICTS.has(districtId) ? 5 : 2)
231
+        .map((row) => ({ reason: "half_point_digit", row })),
232
+    ];
233
+    const seen = new Set();
234
+    for (const bucket of buckets) {
235
+      if (!bucket.row || seen.has(bucket.row.id)) {
236
+        continue;
237
+      }
238
+      seen.add(bucket.row.id);
239
+      const dbRow = dbById.get(bucket.row.id);
240
+      samples.push({
241
+        reason: bucket.reason,
242
+        risk: HIGH_RISK_DISTRICTS.has(districtId) ? "high" : "normal",
243
+        districtId,
244
+        district: DISTRICT_NAMES.get(districtId),
245
+        schoolTarget: dbRow?.SchoolTarget ?? bucket.row.schoolTarget,
246
+        schoolName: dbRow?.SchoolFullName ?? bucket.row.schoolName,
247
+        source: bucket.row.source,
248
+        page: bucket.row.page,
249
+        row: bucket.row.row,
250
+        ScoreTotal: bucket.row.ScoreTotal,
251
+        Score1: bucket.row.Score1,
252
+        Score2: bucket.row.Score2,
253
+        Score3: bucket.row.Score3,
254
+        Score4: bucket.row.Score4,
255
+        officialRemark: bucket.row.officialRemark,
256
+      });
257
+    }
258
+  }
259
+  return samples;
260
+}
261
+
262
+function markdownTable(rows, columns) {
263
+  if (!rows.length) {
264
+    return "无";
265
+  }
266
+  const header = `|${columns.map((column) => column.title).join("|")}|`;
267
+  const divider = `|${columns.map(() => "---").join("|")}|`;
268
+  const body = rows.map((row) => `|${columns.map((column) => String(column.value(row) ?? "").replace(/\|/g, "/")).join("|")}|`);
269
+  return [header, divider, ...body].join("\n");
270
+}
271
+
272
+async function main() {
273
+  const importReports = [];
274
+  for (const file of reportFiles) {
275
+    importReports.push({ file, report: await readJson(file) });
276
+  }
277
+  const expectedRows = collectExpectedRows(importReports);
278
+
279
+  const connection = await mysql.createConnection({
280
+    ...config.database,
281
+    decimalNumbers: true,
282
+    connectTimeout: 10000,
283
+  });
284
+  const [dbRows] = await connection.query(
285
+    `
286
+      SELECT ID, ScoreYear, ScoreType, DistrictID, SchoolTarget, SchoolFullName,
287
+             SchoolTargetRemark, PlanNum, ScoreTotal, Score1, Score2, Score3, Score4,
288
+             ScoreTotalDifferenceValue
289
+      FROM MPS_Score
290
+      WHERE ScoreYear = ?
291
+        AND ScoreType = ?
292
+      ORDER BY DistrictID, CAST(SchoolTarget AS UNSIGNED), SchoolTarget, SchoolFullName
293
+    `,
294
+    [YEAR, SCORE_TYPE],
295
+  );
296
+  const [duplicateRows] = await connection.query(
297
+    `
298
+      SELECT DistrictID, SchoolTarget, SchoolFullName, COALESCE(SchoolTargetRemark, '') AS SchoolTargetRemark, COUNT(*) AS count
299
+      FROM MPS_Score
300
+      WHERE ScoreYear = ?
301
+        AND ScoreType = ?
302
+      GROUP BY DistrictID, SchoolTarget, SchoolFullName, SchoolTargetRemark
303
+      HAVING count > 1
304
+      ORDER BY DistrictID, SchoolTarget
305
+    `,
306
+    [YEAR, SCORE_TYPE],
307
+  );
308
+  await connection.end();
309
+
310
+  const dbById = new Map(dbRows.map((row) => [Number(row.ID), row]));
311
+  const mismatches = compareExpectedToDb(expectedRows, dbById);
312
+  const rangeAnomalies = collectRangeAnomalies(dbRows);
313
+  const samples = selectSamples(expectedRows, dbById);
314
+  const finalReview = await readJson("mps_score_1_15_2026_full_review_report.json");
315
+  const highRiskSamples = samples.filter((sample) => sample.risk === "high");
316
+
317
+  const report = {
318
+    generated_at: new Date().toISOString(),
319
+    scope: { ScoreYear: Number(YEAR), ScoreType: SCORE_TYPE },
320
+    summary: {
321
+      db_rows: dbRows.length,
322
+      expected_source_rows_compared: expectedRows.length,
323
+      value_mismatches: mismatches.length,
324
+      duplicate_key_rows: duplicateRows.length,
325
+      range_anomalies: rangeAnomalies.length,
326
+      deterministic_samples: samples.length,
327
+      high_risk_samples: highRiskSamples.length,
328
+      source_matching_review_blocks: finalReview.source_matching_review.length,
329
+    },
330
+    value_mismatches: mismatches,
331
+    duplicate_key_rows: duplicateRows,
332
+    range_anomalies: rangeAnomalies,
333
+    deterministic_samples: samples,
334
+    high_risk_samples: highRiskSamples,
335
+    manual_visual_checks: MANUAL_VISUAL_CHECKS,
336
+    known_source_matching_review: finalReview.source_matching_review,
337
+    notes: [
338
+      "value_mismatches 为数据库分数字段与导入源报告的逐字段比对结果。",
339
+      "range_anomalies 使用宽松阈值筛查明显 OCR 数字错位,不代表录取规则校验。",
340
+      "high_risk_samples 覆盖扫描 PDF、PDF 图片源、JPEG 或早期 OCR/视觉录入源。",
341
+    ],
342
+  };
343
+  await fs.writeFile(JSON_REPORT, JSON.stringify(report, null, 2), "utf8");
344
+
345
+  const highRiskTable = markdownTable(highRiskSamples.slice(0, 60), [
346
+    { title: "区", value: (row) => row.district },
347
+    { title: "原因", value: (row) => row.reason },
348
+    { title: "学校", value: (row) => row.schoolName },
349
+    { title: "源", value: (row) => row.source },
350
+    { title: "页/行", value: (row) => `${row.page ?? ""}/${row.row ?? ""}` },
351
+    { title: "总分", value: (row) => row.ScoreTotal },
352
+    { title: "语数外", value: (row) => row.Score1 },
353
+    { title: "数学", value: (row) => row.Score2 },
354
+    { title: "语文", value: (row) => row.Score3 },
355
+    { title: "综合", value: (row) => row.Score4 },
356
+    { title: "备注", value: (row) => row.officialRemark },
357
+  ]);
358
+  const sourceReviewLines = finalReview.source_matching_review
359
+    .map((block) => `- ${block.district}: 源${block.source_rows},匹配${block.matched_rows},库${block.db_rows},问题${block.problems.length},库未出现在源${block.db_rows_not_in_source.length}`)
360
+    .join("\n");
361
+  const md = `# 2026 1-15志愿抽样复核报告
362
+
363
+生成时间:${report.generated_at}
364
+
365
+## 机器一致性
366
+- 数据库行数:${dbRows.length}
367
+- 源报告逐字段比对行数:${expectedRows.length}
368
+- 分数字段不一致:${mismatches.length}
369
+- 重复 key:${duplicateRows.length}
370
+- 宽松范围异常:${rangeAnomalies.length}
371
+- 抽样行:${samples.length}
372
+- 高风险抽样行:${highRiskSamples.length}
373
+
374
+## 高风险样本
375
+${highRiskTable}
376
+
377
+## 人工视觉复核
378
+${MANUAL_VISUAL_CHECKS.map((check) => `- ${check.source}: ${check.result}`).join("\n")}
379
+
380
+## 已知源/库不一致
381
+${sourceReviewLines}
382
+
383
+详细清单见 \`mps_score_1_15_2026_sample_audit_report.json\`。
384
+`;
385
+  await fs.writeFile(MD_REPORT, md, "utf8");
386
+  console.log(JSON.stringify(report.summary, null, 2));
387
+}
388
+
389
+main().catch((error) => {
390
+  console.error(error);
391
+  process.exit(1);
392
+});

+ 265 - 0
秒过分数线数据导入/fix_mps_score_1_15_2026_missing_plan_rows.js

@@ -0,0 +1,265 @@
1
+import fs from "fs/promises";
2
+import mysql from "mysql2/promise";
3
+import config from "../src/config/index.js";
4
+
5
+const SCORE_YEAR = "2026";
6
+const SCORE_TYPE = "1-15志愿";
7
+
8
+const planCrossCheck = [
9
+  {
10
+    districtId: 4,
11
+    district: "静安区",
12
+    schoolName: "上海宋庆龄学校",
13
+    officialSource: "静安区-外区.pdf page 2 row 13",
14
+    score: { total: 627, score1: 364, score2: 108, score3: 121, score4: 122 },
15
+    planSource: "静安区2.png",
16
+    planFound: false,
17
+    conclusion: "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。",
18
+  },
19
+  {
20
+    districtId: 4,
21
+    district: "静安区",
22
+    schoolName: "上海奉贤区博华高级中学",
23
+    officialSource: "静安区-外区.pdf page 2 row 16",
24
+    score: { total: 556, score1: 328, score2: 105, score3: 103, score4: 97 },
25
+    planSource: "静安区2.png",
26
+    planFound: false,
27
+    conclusion: "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。",
28
+  },
29
+  {
30
+    districtId: 4,
31
+    district: "静安区",
32
+    schoolName: "上海新纪元双语学校",
33
+    officialSource: "静安区-外区.pdf page 2 row 18",
34
+    score: { total: 483, score1: 271.5, score2: 100, score3: 89, score4: 83 },
35
+    planSource: "静安区2.png",
36
+    planFound: false,
37
+    conclusion: "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。",
38
+  },
39
+  {
40
+    districtId: 7,
41
+    district: "杨浦区",
42
+    schoolName: "复旦大学第二附属中学",
43
+    officialSource: "杨浦区.html row 8",
44
+    score: { total: 696, score1: 406, score2: 133, score3: 134, score4: 145 },
45
+    planSource: "杨浦区1.png + 杨浦区2.png",
46
+    planFound: false,
47
+    conclusion: "分数线源有,本区/外区计划图均未见该校;不补库,留作源文件/计划文件不一致复查。",
48
+  },
49
+  {
50
+    districtId: 13,
51
+    district: "松江区",
52
+    schoolName: "上海创艺高级中学",
53
+    officialCode: "134014",
54
+    officialSource: "松江区.mhtml row 16",
55
+    score: { total: 510.5, score1: 289.5, score2: 84, score3: 111, score4: 89 },
56
+    planSource: "松江区2.png",
57
+    planFound: true,
58
+    planNum: 2,
59
+    schoolTarget: "179",
60
+    conclusion: "分数线源有,计划图也有 2 个计划;数据库缺少计划行,本脚本补入。",
61
+  },
62
+  {
63
+    districtId: 13,
64
+    district: "松江区",
65
+    schoolName: "上海宝山区世外学校",
66
+    officialCode: "135034",
67
+    officialSource: "松江区.mhtml row 20",
68
+    score: { total: 684.5, score1: 399, score2: 142, score3: 119, score4: 142 },
69
+    planSource: "松江区2.png",
70
+    planFound: false,
71
+    conclusion: "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。",
72
+  },
73
+  {
74
+    districtId: 13,
75
+    district: "松江区",
76
+    schoolName: "上海市民办远东学校",
77
+    officialCode: "145007",
78
+    officialSource: "松江区.mhtml row 21",
79
+    score: { total: 579, score1: 331.5, score2: 99, score3: 116, score4: 110 },
80
+    planSource: "松江区2.png",
81
+    planFound: false,
82
+    conclusion: "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。",
83
+  },
84
+];
85
+
86
+const rowsToInsert = planCrossCheck.filter((row) => row.planFound);
87
+
88
+function toNumber(value) {
89
+  if (value === null || value === undefined) return null;
90
+  const number = Number(value);
91
+  return Number.isFinite(number) ? number : null;
92
+}
93
+
94
+async function getPreviousValues(conn, row) {
95
+  const [rows] = await conn.query(
96
+    `SELECT PlanNum, ScoreTotal
97
+       FROM MPS_Score
98
+      WHERE ScoreYear = ?
99
+        AND ScoreType = ?
100
+        AND DistrictID = ?
101
+        AND SchoolTarget = ?
102
+      LIMIT 1`,
103
+    [String(Number(SCORE_YEAR) - 1), SCORE_TYPE, row.districtId, row.schoolTarget],
104
+  );
105
+
106
+  if (!rows.length) {
107
+    return { previousPlan: null, previousScore: null, planDiff: null, scoreDiff: null };
108
+  }
109
+
110
+  const previousPlan = toNumber(rows[0].PlanNum);
111
+  const previousScore = toNumber(rows[0].ScoreTotal);
112
+  return {
113
+    previousPlan,
114
+    previousScore,
115
+    planDiff: previousPlan === null ? null : row.planNum - previousPlan,
116
+    scoreDiff: previousScore === null ? null : row.score.total - previousScore,
117
+  };
118
+}
119
+
120
+async function insertMissingRows(conn) {
121
+  const actions = [];
122
+
123
+  for (const row of rowsToInsert) {
124
+    const [existing] = await conn.query(
125
+      `SELECT *
126
+         FROM MPS_Score
127
+        WHERE ScoreYear = ?
128
+          AND ScoreType = ?
129
+          AND DistrictID = ?
130
+          AND SchoolTarget = ?
131
+        LIMIT 1`,
132
+      [SCORE_YEAR, SCORE_TYPE, row.districtId, row.schoolTarget],
133
+    );
134
+
135
+    if (existing.length) {
136
+      actions.push({ action: "skipped_existing", existing: existing[0], row });
137
+      continue;
138
+    }
139
+
140
+    const previous = await getPreviousValues(conn, row);
141
+    const insertValues = {
142
+      ScoreYear: SCORE_YEAR,
143
+      ScoreType: SCORE_TYPE,
144
+      DistrictID: row.districtId,
145
+      SchoolOfGraduation: null,
146
+      SchoolFullNameJunior: null,
147
+      SchoolTarget: row.schoolTarget,
148
+      SchoolFullName: row.schoolName,
149
+      SchoolTargetRemark: "",
150
+      PlanNum: row.planNum,
151
+      ScoreTotal: row.score.total,
152
+      Score1: row.score.score1,
153
+      Score2: row.score.score2,
154
+      Score3: row.score.score3,
155
+      Score4: row.score.score4,
156
+      SchoolTargetRemark2: null,
157
+      PlanNumDifferenceValue: previous.planDiff,
158
+      ScoreTotalDifferenceValue: previous.scoreDiff,
159
+      OrderID: 0,
160
+      SchoolNumber: "",
161
+      SchoolNumber2: "",
162
+      SchoolOfGraduation1: "0",
163
+    };
164
+
165
+    const columns = Object.keys(insertValues);
166
+    const placeholders = columns.map(() => "?").join(", ");
167
+    const [result] = await conn.query(
168
+      `INSERT INTO MPS_Score (${columns.join(", ")}) VALUES (${placeholders})`,
169
+      columns.map((column) => insertValues[column]),
170
+    );
171
+
172
+    const [insertedRows] = await conn.query("SELECT * FROM MPS_Score WHERE ID = ?", [result.insertId]);
173
+    actions.push({
174
+      action: "inserted",
175
+      insertedId: result.insertId,
176
+      row,
177
+      previous,
178
+      inserted: insertedRows[0],
179
+    });
180
+  }
181
+
182
+  return actions;
183
+}
184
+
185
+function formatValue(value) {
186
+  return value === null || value === undefined ? "" : String(value);
187
+}
188
+
189
+async function writeReport(actions) {
190
+  const report = {
191
+    generated_at: new Date().toISOString(),
192
+    scope: `${SCORE_YEAR} ${SCORE_TYPE} 源有分数线但数据库无计划行的交叉核查`,
193
+    plan_directory: "/Volumes/程杰外接SD盘/上海中考招生计划/2026/计划/1-15志愿",
194
+    rule: "有计划但分数线没见,视为正常未投档;分数线有但库无计划,先查计划图,有计划才补库。",
195
+    checked_rows: planCrossCheck,
196
+    db_actions: actions,
197
+  };
198
+
199
+  await fs.writeFile(
200
+    "mps_score_1_15_2026_plan_crosscheck_report.json",
201
+    `${JSON.stringify(report, null, 2)}\n`,
202
+  );
203
+
204
+  const lines = [
205
+    "# 2026 1-15志愿计划交叉核查报告",
206
+    "",
207
+    `生成时间:${report.generated_at}`,
208
+    "",
209
+    `计划目录:${report.plan_directory}`,
210
+    "",
211
+    "规则:有计划但分数线没见,按未投档处理;分数线有但数据库无计划行,先查计划图,计划图有才补库。",
212
+    "",
213
+    "## 核查结论",
214
+    "",
215
+    "|区|学校|分数线源位置|计划图|计划图是否有|处理|",
216
+    "|---|---|---|---|---|---|",
217
+    ...planCrossCheck.map((row) =>
218
+      [
219
+        row.district,
220
+        row.schoolName,
221
+        row.officialSource,
222
+        row.planSource,
223
+        row.planFound ? `有,计划数 ${row.planNum}` : "未见",
224
+        row.conclusion,
225
+      ]
226
+        .map((cell) => String(cell).replaceAll("|", "\\|"))
227
+        .join("|")
228
+        .replace(/^/, "|")
229
+        .replace(/$/, "|"),
230
+    ),
231
+    "",
232
+    "## 数据库动作",
233
+    "",
234
+    ...actions.map((action) => {
235
+      if (action.action === "inserted") {
236
+        const inserted = action.inserted;
237
+        return `- 已补入 ${action.row.district} ${action.row.schoolName}: ID ${action.insertedId}, SchoolTarget ${inserted.SchoolTarget}, PlanNum ${inserted.PlanNum}, ScoreTotal ${formatValue(inserted.ScoreTotal)}, Score1 ${formatValue(inserted.Score1)}, Score2 ${formatValue(inserted.Score2)}, Score3 ${formatValue(inserted.Score3)}, Score4 ${formatValue(inserted.Score4)}, PlanDiff ${formatValue(inserted.PlanNumDifferenceValue)}, ScoreDiff ${formatValue(inserted.ScoreTotalDifferenceValue)}`;
238
+      }
239
+      return `- 已存在,跳过 ${action.row.district} ${action.row.schoolName}: ID ${action.existing.ID}`;
240
+    }),
241
+    "",
242
+  ];
243
+
244
+  await fs.writeFile("mps_score_1_15_2026_plan_crosscheck_report.md", `${lines.join("\n")}\n`);
245
+}
246
+
247
+async function main() {
248
+  const conn = await mysql.createConnection({
249
+    host: config.database.host,
250
+    user: config.database.user,
251
+    password: config.database.password,
252
+    database: config.database.database,
253
+    port: config.database.port,
254
+  });
255
+
256
+  try {
257
+    const actions = await insertMissingRows(conn);
258
+    await writeReport(actions);
259
+    console.log(JSON.stringify({ checked: planCrossCheck.length, actions }, null, 2));
260
+  } finally {
261
+    await conn.end();
262
+  }
263
+}
264
+
265
+await main();

+ 421 - 0
秒过分数线数据导入/generate_mps_score_1_15_2026_full_review_report.js

@@ -0,0 +1,421 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const SCORE_TYPE = "1-15志愿";
11
+
12
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
+const JSON_REPORT = path.join(SCRIPT_DIR, "mps_score_1_15_2026_full_review_report.json");
14
+const MD_REPORT = path.join(SCRIPT_DIR, "mps_score_1_15_2026_full_review_report.md");
15
+
16
+const DISTRICT_NAMES = new Map([
17
+  [1, "黄浦区"],
18
+  [2, "徐汇区"],
19
+  [3, "长宁区"],
20
+  [4, "静安区"],
21
+  [5, "普陀区"],
22
+  [6, "虹口区"],
23
+  [7, "杨浦区"],
24
+  [8, "闵行区"],
25
+  [9, "宝山区"],
26
+  [10, "嘉定区"],
27
+  [11, "浦东新区"],
28
+  [12, "金山区"],
29
+  [13, "松江区"],
30
+  [14, "青浦区"],
31
+  [15, "奉贤区"],
32
+  [16, "崇明区"],
33
+]);
34
+
35
+const LATEST_CHECKS = [
36
+  {
37
+    district: "徐汇区",
38
+    source: "徐汇区.pdf",
39
+    result: "已按3页PDF重跑,68行全匹配,补录第3页23行;changed_rows=24",
40
+  },
41
+  {
42
+    district: "长宁区",
43
+    source: "长宁区.pdf",
44
+    result: "57行全匹配;库内另有上海新纪元双语学校计划行,源PDF未列,按未投档保留0分;changed_rows=0",
45
+  },
46
+  {
47
+    district: "黄浦区",
48
+    source: "黄浦区.pdf",
49
+    result: "67行全匹配;changed_rows=0",
50
+  },
51
+  {
52
+    district: "虹口区",
53
+    source: "虹口区.mhtml",
54
+    result: "50行全匹配;changed_rows=0",
55
+  },
56
+  {
57
+    district: "闵行区",
58
+    source: "闵行区.pdf",
59
+    result: "80行全匹配;PDF与图片版分数字段一致;changed_rows=0",
60
+  },
61
+  {
62
+    district: "青浦区",
63
+    source: "青浦区.html",
64
+    result: "54行全匹配;当前HTML与数据库一致;changed_rows=0",
65
+  },
66
+  {
67
+    district: "松江区",
68
+    source: "松江区2.png + 松江区.mhtml",
69
+    result: "计划图确认上海创艺高级中学有2个计划,已补入数据库 ID 40207;剩余源有库无2条在计划图未见。",
70
+  },
71
+];
72
+
73
+const MANUAL_VISUAL_CHECKS = [
74
+  {
75
+    source: "宝山区.pdf page 2",
76
+    result: "确认上海民办民一中学为 483 / 311 / 70 / 110 / 59,综合测试 59 不是水印误读。",
77
+  },
78
+  {
79
+    source: "崇明区1.jpeg",
80
+    result: "确认上海民办民一中学为 441.5 / 228 / 40 / 100 / 82,上海新纪元双语学校为 464.5 / 236.5 / 67 / 98 / 94。",
81
+  },
82
+  {
83
+    source: "浦东新区.pdf page 2-3",
84
+    result: "确认低分段和未录满行,包括东鼎外国语 480.5 / 262.5 / 57 / 105 / 92,民一中学 483.5 / 242.5 / 74 / 117 / 105,新纪元 480.5 / 259.5 / 92 / 122 / 85。",
85
+  },
86
+  {
87
+    source: "徐汇区.pdf page 3",
88
+    result: "确认补录第 3 页 23 行,重点核对浦实 620.5、东鼎 576.5、励滕线上无人投档、民一 472、新纪元 517.5。",
89
+  },
90
+  {
91
+    source: "青浦区.html",
92
+    result: "确认当前 HTML 54 行全匹配数据库,宏润博源为 493.5,不是 0 分;正式核查 changed_rows=0。",
93
+  },
94
+  {
95
+    source: "计划/1-15志愿/静安区2.png、杨浦区1-2.png、松江区2.png",
96
+    result: "确认源有库无的7条中,只有松江上海创艺高级中学在计划图中出现;其余6条计划图未见。",
97
+  },
98
+];
99
+
100
+async function readJson(file, fallback = null) {
101
+  try {
102
+    return JSON.parse(await fs.readFile(path.join(SCRIPT_DIR, file), "utf8"));
103
+  } catch (error) {
104
+    if (fallback !== null && error.code === "ENOENT") {
105
+      return fallback;
106
+    }
107
+    throw error;
108
+  }
109
+}
110
+
111
+function num(value) {
112
+  if (value === null || value === undefined || value === "") return null;
113
+  const parsed = Number(value);
114
+  return Number.isFinite(parsed) ? parsed : null;
115
+}
116
+
117
+function simpleRow(row) {
118
+  return {
119
+    ID: row.ID,
120
+    SchoolTarget: row.SchoolTarget,
121
+    SchoolFullName: row.SchoolFullName,
122
+    SchoolTargetRemark: row.SchoolTargetRemark ?? "",
123
+    PlanNum: num(row.PlanNum),
124
+    ScoreTotal: num(row.ScoreTotal),
125
+  };
126
+}
127
+
128
+function normalizeBatchReview(district) {
129
+  return {
130
+    district_id: district.district_id,
131
+    district: district.district,
132
+    source: district.source,
133
+    source_rows: district.source_rows,
134
+    matched_rows: district.matched_rows,
135
+    db_rows: district.db_rows,
136
+    problems: district.problems ?? [],
137
+    db_rows_not_in_source: district.not_in_source ?? [],
138
+  };
139
+}
140
+
141
+function normalizeSpecificReview(report) {
142
+  const summary = report.summary;
143
+  const notInOfficial = report.not_in_official_image ?? [];
144
+  const problems = report.problems ?? [];
145
+  const source = report.source_pdf ?? report.source_image ?? report.source_images ?? summary?.district ?? "";
146
+  return {
147
+    district_id: summary.district_id,
148
+    district: summary.district,
149
+    source,
150
+    source_rows: summary.official_rows,
151
+    matched_rows: summary.official_rows - problems.length,
152
+    db_rows: summary.db_rows,
153
+    problems,
154
+    db_rows_not_in_source: notInOfficial,
155
+  };
156
+}
157
+
158
+function collectSourceMatchingReview(batchReport, specificReports) {
159
+  const reviews = [];
160
+
161
+  for (const district of batchReport.districts ?? []) {
162
+    if ((district.problem_rows ?? 0) > 0 || (district.not_in_source_rows ?? 0) > 0) {
163
+      reviews.push(normalizeBatchReview(district));
164
+    }
165
+  }
166
+
167
+  for (const report of specificReports) {
168
+    const summary = report.summary;
169
+    if (!summary) continue;
170
+    const hasIssues = (summary.not_in_official_image_rows ?? 0) > 0 || (report.problems?.length ?? 0) > 0;
171
+    if (hasIssues && !reviews.some((review) => review.district_id === summary.district_id)) {
172
+      reviews.push(normalizeSpecificReview(report));
173
+    }
174
+  }
175
+
176
+  return reviews.sort((a, b) => a.district_id - b.district_id);
177
+}
178
+
179
+function markdownTable(rows, columns) {
180
+  if (!rows.length) return "无";
181
+  const header = `|${columns.map((column) => column.title).join("|")}|`;
182
+  const divider = `|${columns.map(() => "---").join("|")}|`;
183
+  const body = rows.map((row) => `|${columns.map((column) => String(column.value(row) ?? "").replaceAll("|", "/")).join("|")}|`);
184
+  return [header, divider, ...body].join("\n");
185
+}
186
+
187
+async function collectDbReview(connection) {
188
+  const [databaseSummary] = await connection.query(
189
+    `
190
+      SELECT
191
+        DistrictID,
192
+        COUNT(*) AS db_rows,
193
+        COALESCE(SUM(PlanNum), 0) AS plan_sum,
194
+        SUM(CASE WHEN ScoreTotal > 0 THEN 1 ELSE 0 END) AS scored_rows,
195
+        SUM(CASE WHEN ScoreTotal = 0 THEN 1 ELSE 0 END) AS zero_rows,
196
+        SUM(CASE WHEN ScoreTotal > 0 AND Score1 IS NULL THEN 1 ELSE 0 END) AS total_only_rows,
197
+        MIN(CASE WHEN ScoreTotal > 0 THEN ScoreTotal END) AS min_score,
198
+        MAX(CASE WHEN ScoreTotal > 0 THEN ScoreTotal END) AS max_score
199
+      FROM MPS_Score
200
+      WHERE ScoreYear = ?
201
+        AND ScoreType = ?
202
+      GROUP BY DistrictID
203
+      ORDER BY DistrictID
204
+    `,
205
+    [YEAR, SCORE_TYPE],
206
+  );
207
+
208
+  const [duplicateRows] = await connection.query(
209
+    `
210
+      SELECT DistrictID, SchoolTarget, SchoolFullName, COALESCE(SchoolTargetRemark, '') AS SchoolTargetRemark, COUNT(*) AS count
211
+      FROM MPS_Score
212
+      WHERE ScoreYear = ?
213
+        AND ScoreType = ?
214
+      GROUP BY DistrictID, SchoolTarget, SchoolFullName, SchoolTargetRemark
215
+      HAVING count > 1
216
+      ORDER BY DistrictID, SchoolTarget
217
+    `,
218
+    [YEAR, SCORE_TYPE],
219
+  );
220
+
221
+  const [zeroRows] = await connection.query(
222
+    `
223
+      SELECT ID, DistrictID, SchoolTarget, SchoolFullName, COALESCE(SchoolTargetRemark, '') AS SchoolTargetRemark,
224
+             PlanNum, ScoreTotal, ScoreTotalDifferenceValue
225
+      FROM MPS_Score
226
+      WHERE ScoreYear = ?
227
+        AND ScoreType = ?
228
+        AND ScoreTotal = 0
229
+      ORDER BY DistrictID, CAST(SchoolTarget AS UNSIGNED), SchoolTarget
230
+    `,
231
+    [YEAR, SCORE_TYPE],
232
+  );
233
+
234
+  const [zeroRowsWithDiff] = await connection.query(
235
+    `
236
+      SELECT ID, DistrictID, SchoolTarget, SchoolFullName, COALESCE(SchoolTargetRemark, '') AS SchoolTargetRemark,
237
+             PlanNum, ScoreTotal, ScoreTotalDifferenceValue
238
+      FROM MPS_Score
239
+      WHERE ScoreYear = ?
240
+        AND ScoreType = ?
241
+        AND ScoreTotal = 0
242
+        AND ScoreTotalDifferenceValue IS NOT NULL
243
+        AND ScoreTotalDifferenceValue <> 0
244
+      ORDER BY DistrictID, CAST(SchoolTarget AS UNSIGNED), SchoolTarget
245
+    `,
246
+    [YEAR, SCORE_TYPE],
247
+  );
248
+
249
+  const [totalOnlyRows] = await connection.query(
250
+    `
251
+      SELECT ID, DistrictID, SchoolTarget, SchoolFullName, COALESCE(SchoolTargetRemark, '') AS SchoolTargetRemark,
252
+             PlanNum, ScoreTotal, ScoreTotalDifferenceValue
253
+      FROM MPS_Score
254
+      WHERE ScoreYear = ?
255
+        AND ScoreType = ?
256
+        AND ScoreTotal > 0
257
+        AND Score1 IS NULL
258
+      ORDER BY DistrictID, CAST(SchoolTarget AS UNSIGNED), SchoolTarget
259
+    `,
260
+    [YEAR, SCORE_TYPE],
261
+  );
262
+
263
+  return {
264
+    databaseSummary: databaseSummary.map((row) => ({
265
+      DistrictID: row.DistrictID,
266
+      DistrictName: DISTRICT_NAMES.get(Number(row.DistrictID)),
267
+      db_rows: num(row.db_rows),
268
+      plan_sum: num(row.plan_sum),
269
+      scored_rows: num(row.scored_rows),
270
+      zero_rows: num(row.zero_rows),
271
+      total_only_rows: num(row.total_only_rows),
272
+      min_score: num(row.min_score),
273
+      max_score: num(row.max_score),
274
+    })),
275
+    duplicateRows,
276
+    zeroRows: zeroRows.map((row) => ({ ...simpleRow(row), DistrictName: DISTRICT_NAMES.get(Number(row.DistrictID)), ScoreTotalDifferenceValue: num(row.ScoreTotalDifferenceValue) })),
277
+    zeroRowsWithDiff: zeroRowsWithDiff.map((row) => ({ ...simpleRow(row), DistrictName: DISTRICT_NAMES.get(Number(row.DistrictID)), ScoreTotalDifferenceValue: num(row.ScoreTotalDifferenceValue) })),
278
+    totalOnlyRows: totalOnlyRows.map((row) => ({ ...simpleRow(row), DistrictName: DISTRICT_NAMES.get(Number(row.DistrictID)), ScoreTotalDifferenceValue: num(row.ScoreTotalDifferenceValue) })),
279
+  };
280
+}
281
+
282
+async function main() {
283
+  const batchReport = await readJson("mps_score_1_15_batch_import_report_2026.json");
284
+  const specificReports = await Promise.all([
285
+    readJson("mps_score_1_15_xuhui_scores_2026_import_report.json"),
286
+    readJson("mps_score_1_15_baoshan_scores_2026_import_report.json"),
287
+    readJson("mps_score_1_15_pudong_scores_2026_import_report.json"),
288
+    readJson("mps_score_1_15_qingpu_scores_2026_import_report.json"),
289
+  ]);
290
+  const sampleAudit = await readJson("mps_score_1_15_2026_sample_audit_report.json", { summary: null });
291
+  const planCrossCheck = await readJson("mps_score_1_15_2026_plan_crosscheck_report.json", { checked_rows: [], db_actions: [] });
292
+
293
+  const connection = await mysql.createConnection({
294
+    ...config.database,
295
+    decimalNumbers: true,
296
+    connectTimeout: 10000,
297
+  });
298
+  const dbReview = await collectDbReview(connection);
299
+  await connection.end();
300
+
301
+  const sourceMatchingReview = collectSourceMatchingReview(batchReport, specificReports);
302
+  const totalDbRows = dbReview.databaseSummary.reduce((sum, row) => sum + row.db_rows, 0);
303
+  const totalPlan = dbReview.databaseSummary.reduce((sum, row) => sum + row.plan_sum, 0);
304
+  const auditSummary = sampleAudit.summary
305
+    ? {
306
+        ...sampleAudit.summary,
307
+        source_matching_review_blocks: sourceMatchingReview.length,
308
+      }
309
+    : {
310
+        db_rows: totalDbRows,
311
+        expected_source_rows_compared: null,
312
+        value_mismatches: null,
313
+        duplicate_key_rows: dbReview.duplicateRows.length,
314
+        range_anomalies: null,
315
+        deterministic_samples: null,
316
+        high_risk_samples: null,
317
+        source_matching_review_blocks: sourceMatchingReview.length,
318
+      };
319
+
320
+  const report = {
321
+    generated_at: new Date().toISOString(),
322
+    scope: { ScoreYear: Number(YEAR), ScoreType: SCORE_TYPE },
323
+    latest_checks: LATEST_CHECKS,
324
+    audit_summary: auditSummary,
325
+    database_summary: dbReview.databaseSummary,
326
+    duplicate_key_rows: dbReview.duplicateRows,
327
+    manual_visual_checks: MANUAL_VISUAL_CHECKS,
328
+    source_matching_review: sourceMatchingReview,
329
+    plan_crosscheck: {
330
+      report_files: [
331
+        "mps_score_1_15_2026_plan_crosscheck_report.json",
332
+        "mps_score_1_15_2026_plan_crosscheck_report.md",
333
+      ],
334
+      checked_rows: planCrossCheck.checked_rows ?? [],
335
+      db_actions: planCrossCheck.db_actions ?? [],
336
+    },
337
+    zero_score_rows_for_review: dbReview.zeroRows,
338
+    zero_score_rows_with_nonzero_difference_for_review: dbReview.zeroRowsWithDiff,
339
+    total_only_rows_for_review: dbReview.totalOnlyRows,
340
+    generated_files: [
341
+      "mps_score_1_15_2026_full_review_report.json",
342
+      "mps_score_1_15_2026_full_review_report.md",
343
+      "mps_score_1_15_2026_sample_audit_report.json",
344
+      "mps_score_1_15_2026_sample_audit_report.md",
345
+      "mps_score_1_15_2026_plan_crosscheck_report.json",
346
+      "mps_score_1_15_2026_plan_crosscheck_report.md",
347
+    ],
348
+    notes: [
349
+      "库有计划但分数线源未列,按未投档/无学生报入处理,保留库内0分行,不再视为错误。",
350
+      "分数线源有但数据库无计划行时,需查计划图;计划图有才补库,计划图未见则列为源文件/计划文件不一致复查。",
351
+      "2026计划交叉核查已补入松江区上海创艺高级中学,其余6条源有库无在计划图未见。",
352
+    ],
353
+  };
354
+
355
+  await fs.writeFile(JSON_REPORT, `${JSON.stringify(report, null, 2)}\n`, "utf8");
356
+
357
+  const dbTable = markdownTable(dbReview.databaseSummary, [
358
+    { title: "区", value: (row) => row.DistrictName },
359
+    { title: "DB行", value: (row) => row.db_rows },
360
+    { title: "计划数", value: (row) => row.plan_sum },
361
+    { title: "有分", value: (row) => row.scored_rows },
362
+    { title: "0分", value: (row) => row.zero_rows },
363
+    { title: "总分-only", value: (row) => row.total_only_rows },
364
+    { title: "最低", value: (row) => row.min_score },
365
+    { title: "最高", value: (row) => row.max_score },
366
+  ]);
367
+  const sourceReviewLines = sourceMatchingReview
368
+    .map((block) => `- ${block.district}: 源${block.source_rows},匹配${block.matched_rows},库${block.db_rows},源有库无${block.problems.length},库有源无${block.db_rows_not_in_source.length}`)
369
+    .join("\n");
370
+  const planLines = (planCrossCheck.checked_rows ?? [])
371
+    .map((row) => `- ${row.district} ${row.schoolName}: ${row.planFound ? `计划图有,计划数${row.planNum}` : "计划图未见"};${row.conclusion}`)
372
+    .join("\n");
373
+
374
+  const md = `# 2026 1-15志愿录入复查报告
375
+
376
+生成时间:${report.generated_at}
377
+
378
+## 最新处理
379
+${LATEST_CHECKS.map((check) => `- ${check.district}: ${check.result}`).join("\n")}
380
+
381
+## 抽样审计
382
+- 数据库行数:${auditSummary.db_rows ?? totalDbRows}
383
+- 源报告逐字段比对行数:${auditSummary.expected_source_rows_compared ?? ""}
384
+- 分数字段不一致:${auditSummary.value_mismatches ?? ""}
385
+- 重复 key:${auditSummary.duplicate_key_rows ?? dbReview.duplicateRows.length}
386
+- 高风险抽样行:${auditSummary.high_risk_samples ?? ""}
387
+- 明显异常候选:${auditSummary.range_anomalies ?? ""}
388
+
389
+## 人工视觉复核
390
+${MANUAL_VISUAL_CHECKS.map((check) => `- ${check.source}: ${check.result}`).join("\n")}
391
+
392
+## 数据库汇总
393
+总行数:${totalDbRows};总计划数:${totalPlan}
394
+
395
+${dbTable}
396
+
397
+## 需优先复查
398
+- 重复 key:${dbReview.duplicateRows.length} 条
399
+- 源文件/数据库不一致区块:${sourceMatchingReview.length} 个
400
+- 0分行:${dbReview.zeroRows.length} 条
401
+- 0分但前年差非空非0:${dbReview.zeroRowsWithDiff.length} 条
402
+- 总分-only 行:${dbReview.totalOnlyRows.length} 条
403
+
404
+## 源文件/数据库不一致
405
+说明:库有源无按“有计划但未投档”保留,不作为错误;重点看“源有库无”。
406
+
407
+${sourceReviewLines}
408
+
409
+## 计划交叉核查
410
+${planLines}
411
+
412
+详细清单见 \`mps_score_1_15_2026_full_review_report.json\`。
413
+`;
414
+  await fs.writeFile(MD_REPORT, md, "utf8");
415
+  console.log(JSON.stringify({ totalDbRows, totalPlan, source_matching_review_blocks: sourceMatchingReview.length }, null, 2));
416
+}
417
+
418
+main().catch((error) => {
419
+  console.error(error);
420
+  process.exit(1);
421
+});

+ 389 - 0
秒过分数线数据导入/import_mps_score_1_15_baoshan_scores_2026.js

@@ -0,0 +1,389 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const PREVIOUS_YEAR = "2025";
11
+const SCORE_TYPE = "1-15志愿";
12
+const DISTRICT_ID = 9;
13
+const DISTRICT_NAME = "宝山区";
14
+
15
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
16
+const REPORT_FILE = path.join(SCRIPT_DIR, "mps_score_1_15_baoshan_scores_2026_import_report.json");
17
+
18
+const SCORE_COLUMNS = [
19
+  "ScoreTotal",
20
+  "Score1",
21
+  "Score2",
22
+  "Score3",
23
+  "Score4",
24
+  "ScoreTotalDifferenceValue",
25
+];
26
+
27
+const OFFICIAL_ROWS = [
28
+  { page: 1, row: 1, officialCode: "132001", schoolTarget: "47", schoolName: "上海市行知中学", scoreTotal: 695, score1: 410, score2: 138, score3: 127, score4: 140 },
29
+  { page: 1, row: 2, officialCode: "132002", schoolTarget: "48", schoolName: "上海大学附属中学", scoreTotal: 678, score1: 396, score2: 127, score3: 129, score4: 138 },
30
+  { page: 1, row: 3, officialCode: "132003", schoolTarget: "50", schoolName: "上海师范大学附属中学宝山分校", scoreTotal: 691, score1: 405, score2: 140, score3: 122, score4: 142 },
31
+  { page: 1, row: 4, officialCode: "133001", schoolTarget: "49", schoolName: "上海市吴淞中学", scoreTotal: 681.5, score1: 399.5, score2: 130, score3: 124, score4: 143 },
32
+  { page: 1, row: 5, officialCode: "133002", schoolTarget: "167", schoolName: "上海师范大学附属宝山罗店中学", scoreTotal: 659.5, score1: 385.5, score2: 136, score3: 113, score4: 130 },
33
+  { page: 1, row: 6, officialCode: "133003", schoolTarget: "51", schoolName: "华东师范大学第二附属中学(宝山校区)", scoreTotal: 704.5, score1: 412.5, score2: 137, score3: 128, score4: 146 },
34
+  { page: 1, row: 7, officialCode: "134001", schoolTarget: "168", schoolName: "上海市宝山中学", scoreTotal: 645, score1: 378, score2: 131, score3: 111, score4: 130 },
35
+  { page: 1, row: 8, officialCode: "134002", schoolTarget: "169", schoolName: "上海市通河中学", scoreTotal: 636.5, score1: 369, score2: 112, score3: 122, score4: 129 },
36
+  { page: 1, row: 9, officialCode: "134003", schoolTarget: "172", schoolName: "上海市淞浦中学", scoreTotal: 575.5, score1: 329.5, score2: 113, score3: 126, score4: 105 },
37
+  { page: 1, row: 10, officialCode: "134004", schoolTarget: "170", schoolName: "上海市顾村中学", scoreTotal: 620.5, score1: 355, score2: 105, score3: 121, score4: 127 },
38
+  { page: 1, row: 11, officialCode: "134005", schoolTarget: "171", schoolName: "上海市行知实验中学", scoreTotal: 611, score1: 366.5, score2: 116, score3: 117, score4: 109 },
39
+  { page: 1, row: 12, officialCode: "134006", schoolTarget: "173", schoolName: "上海市高境第一中学", scoreTotal: 599.5, score1: 346, score2: 101, score3: 116, score4: 123 },
40
+  { page: 1, row: 13, officialCode: "134009", schoolTarget: "174", schoolName: "上海市宝山区海滨中学", scoreTotal: 590.5, score1: 331.5, score2: 104, score3: 103, score4: 127 },
41
+  { page: 1, row: 14, officialCode: "134010", schoolTarget: "851", schoolName: "上海民办行中中学", scoreTotal: 480 },
42
+  { page: 1, row: 15, officialCode: "134012", schoolTarget: "177", schoolName: "上海存志高级中学", scoreTotal: 591, score1: 346.5, score2: 97, score3: 124, score4: 112 },
43
+  { page: 1, row: 16, officialCode: "134013", schoolTarget: "178", schoolName: "上海宝山区民办维尚高级中学", scoreTotal: 501 },
44
+  { page: 1, row: 17, officialCode: "134014", schoolTarget: "179", schoolName: "上海创艺高级中学", scoreTotal: 485 },
45
+  { page: 1, row: 18, officialCode: "134015", schoolTarget: "180", schoolName: "上海市宝山华曜高级中学", scoreTotal: 527.5, score1: 307.5, score2: 90, score3: 111, score4: 91 },
46
+  { page: 1, row: 19, officialCode: "135001", schoolTarget: "175", schoolName: "上海市同洲模范学校", scoreTotal: 501 },
47
+  { page: 1, row: 20, officialCode: "135003", schoolTarget: "852", schoolName: "上海金瑞学校", scoreTotal: 501 },
48
+  { page: 1, row: 21, officialCode: "135033", schoolTarget: "1028", schoolName: "上海民办至德实验学校", scoreTotal: 621.5, score1: 358.5, score2: 101, score3: 124, score4: 128 },
49
+  { page: 1, row: 22, officialCode: "135034", schoolTarget: "176", schoolName: "上海宝山区世外学校", scoreTotal: 670.5, score1: 394.5, score2: 126, score3: 125, score4: 135 },
50
+  { page: 1, row: 23, officialCode: "014006", schoolTarget: "81", schoolName: "上海市同济黄浦设计创意中学", scoreTotal: 659, score1: 373.5, score2: 111, score3: 119, score4: 140 },
51
+  { page: 1, row: 24, officialCode: "042032", schoolTarget: "1", schoolName: "上海市上海中学", scoreTotal: 717.5, score1: 426, score2: 150, score3: 131, score4: 145 },
52
+  { page: 1, row: 25, officialCode: "063008", schoolTarget: "105", schoolName: "上海市久隆模范中学", scoreTotal: 633.5, score1: 372, score2: 118, score3: 119, score4: 120 },
53
+  { page: 1, row: 26, officialCode: "064020", schoolTarget: "108", schoolName: "上海田家炳中学", scoreTotal: 656, score1: 377, score2: 130, score3: 108, score4: 141 },
54
+  { page: 1, row: 27, officialCode: "064021", schoolTarget: "111", schoolName: "上海市民办扬波中学", scoreTotal: 573.5, score1: 336.5, score2: 86, score3: 111, score4: 108 },
55
+  { page: 1, row: 28, officialCode: "064024", schoolTarget: "113", schoolName: "上海市民办风范中学", scoreTotal: 568, score1: 337, score2: 98, score3: 112, score4: 107 },
56
+  { page: 1, row: 29, officialCode: "069004", schoolTarget: "100", schoolName: "上海戏剧学院附属高级中学", remark: "(艺术班)", scoreTotal: 616.5, score1: 359, score2: 112, score3: 119, score4: 118 },
57
+  { page: 1, row: 30, officialCode: "074081", schoolTarget: "122", schoolName: "上海安生学校", scoreTotal: 501 },
58
+  { page: 2, row: 1, officialCode: "079016", schoolTarget: "117", schoolName: "上海音乐学院附属安师实验中学", remark: "(艺术班)", scoreTotal: 580.5, score1: 333.5, score2: 87, score3: 121, score4: 113 },
59
+  { page: 2, row: 2, officialCode: "102056", schoolTarget: "2", schoolName: "上海交通大学附属中学", scoreTotal: 717, score1: 423, score2: 146, score3: 129, score4: 149 },
60
+  { page: 2, row: 3, officialCode: "102057", schoolTarget: "3", schoolName: "复旦大学附属中学", scoreTotal: 717, score1: 424, score2: 144, score3: 134, score4: 147 },
61
+  { page: 2, row: 4, officialCode: "124116", schoolTarget: "156", schoolName: "上海市民办燎原双语高级中学", scoreTotal: 608.5, score1: 346.5, score2: 103, score3: 117, score4: 120 },
62
+  { page: 2, row: 5, officialCode: "145007", schoolTarget: "188", schoolName: "上海市民办远东学校", scoreTotal: 567.5, score1: 338.5, score2: 99, score3: 111, score4: 105 },
63
+  { page: 2, row: 6, officialCode: "145018", schoolTarget: "189", schoolName: "上海华旭双语学校", scoreTotal: 491 },
64
+  { page: 2, row: 7, officialCode: "145030", schoolTarget: "190", schoolName: "上海嘉定区民办华盛怀少学校", scoreTotal: 501 },
65
+  { page: 2, row: 8, officialCode: "152003", schoolTarget: "4", schoolName: "华东师范大学第二附属中学", scoreTotal: 716.5, score1: 428, score2: 150, score3: 130, score4: 147 },
66
+  { page: 2, row: 9, officialCode: "154039", schoolTarget: "220", schoolName: "上海市民办丰华高级中学", scoreTotal: 559, score1: 329, score2: 107, score3: 116, score4: 97 },
67
+  { page: 2, row: 10, officialCode: "154048", schoolTarget: "223", schoolName: "民办上海工商外国语职业学院附属中学", scoreTotal: 562.5, score1: 335, score2: 95, score3: 118, score4: 97 },
68
+  { page: 2, row: 11, officialCode: "155041", schoolTarget: "229", schoolName: "上海浦东新区民办东鼎外国语学校", scoreTotal: 553.5, score1: 315, score2: 77, score3: 119, score4: 106 },
69
+  { page: 2, row: 12, officialCode: "155043", schoolTarget: "230", schoolName: "上海市民办尚德实验学校", scoreTotal: 630, score1: 366, score2: 105, score3: 124, score4: 119 },
70
+  { page: 2, row: 13, officialCode: "164005", schoolTarget: "243", schoolName: "上海市民办交大南洋中学", scoreTotal: 501 },
71
+  { page: 2, row: 14, officialCode: "164008", schoolTarget: "246", schoolName: "上海市民办永昌中学", scoreTotal: 501 },
72
+  { page: 2, row: 15, officialCode: "165004", schoolTarget: "244", schoolName: "上海金山区世外学校", scoreTotal: 662, score1: 397.5, score2: 129, score3: 127, score4: 128 },
73
+  { page: 2, row: 16, officialCode: "173005", schoolTarget: "1034", schoolName: "上海市松江区励滕高级中学", scoreTotal: 501 },
74
+  { page: 2, row: 17, officialCode: "174013", schoolTarget: "860", schoolName: "上海领科双语学校", scoreTotal: 603.5, score1: 353, score2: 116, score3: 113, score4: 115 },
75
+  { page: 2, row: 18, officialCode: "175018", schoolTarget: "251", schoolName: "上海市西外外国语学校", scoreTotal: 504.5, score1: 287, score2: 86, score3: 103, score4: 89 },
76
+  { page: 2, row: 19, officialCode: "175024", schoolTarget: "254", schoolName: "上海赫贤学校", scoreTotal: 501 },
77
+  { page: 2, row: 20, officialCode: "175025", schoolTarget: "995", schoolName: "上海松江区爱菊学校", scoreTotal: 501 },
78
+  { page: 2, row: 21, officialCode: "184006", schoolTarget: "863", schoolName: "上海青浦区世外高级中学", scoreTotal: 533, score1: 303, score2: 91, score3: 98, score4: 99.5 },
79
+  { page: 2, row: 22, officialCode: "184007", schoolTarget: "862", schoolName: "上海青浦区宏润博源高级中学", scoreTotal: 491 },
80
+  { page: 2, row: 23, officialCode: "185014", schoolTarget: "260", schoolName: "上海青浦区协和双语学校", scoreTotal: 501 },
81
+  { page: 2, row: 24, officialCode: "204020", schoolTarget: "267", schoolName: "上海美达菲双语高级中学", scoreTotal: 501 },
82
+  { page: 2, row: 25, officialCode: "204021", schoolTarget: "266", schoolName: "上海奉贤区博华高级中学", scoreTotal: 558, score1: 309.5, score2: 100, score3: 109, score4: 109 },
83
+  { page: 2, row: 26, officialCode: "514001", schoolTarget: "268", schoolName: "上海市崇明区城桥中学", scoreTotal: 564, score1: 326.5, score2: 104, score3: 108, score4: 100 },
84
+  { page: 2, row: 27, officialCode: "514004", schoolTarget: "271", schoolName: "上海市崇明区堡镇中学", scoreTotal: 599.5, score1: 347, score2: 100, score3: 118, score4: 115 },
85
+  { page: 2, row: 28, officialCode: "514010", schoolTarget: "272", schoolName: "上海民办民一中学", scoreTotal: 483, score1: 311, score2: 70, score3: 110, score4: 59 },
86
+  { page: 2, row: 29, officialCode: "514014", schoolTarget: "1039", schoolName: "上海新纪元双语学校", scoreTotal: 462 },
87
+];
88
+
89
+const decimalOrNull = (value) => (value === undefined || value === null || value === "" ? null : Number(value));
90
+const scoreValue = (row, column) => decimalOrNull(row[column]);
91
+const scoreKey = (row) => `${row.SchoolTarget ?? row.schoolTarget}\t${row.SchoolTargetRemark ?? row.remark ?? ""}`;
92
+const toFixed2 = (value) => (value === null || value === undefined ? null : Number(value).toFixed(2));
93
+
94
+function assertUniqueOfficialRows() {
95
+  const seen = new Set();
96
+  for (const row of OFFICIAL_ROWS) {
97
+    const key = scoreKey(row);
98
+    if (seen.has(key)) {
99
+      throw new Error(`duplicate official row key: ${key}`);
100
+    }
101
+    seen.add(key);
102
+  }
103
+}
104
+
105
+function diffFromPrevious(currentScore, previousRow) {
106
+  if (!previousRow || previousRow.ScoreTotal === null || previousRow.ScoreTotal === undefined || previousRow.ScoreTotal === "") {
107
+    return null;
108
+  }
109
+  return Number((Number(currentScore) - Number(previousRow.ScoreTotal)).toFixed(2));
110
+}
111
+
112
+function buildUpdate(officialRow, dbRow, previousRow) {
113
+  const scoreTotal = scoreValue(officialRow, "scoreTotal");
114
+  return {
115
+    id: dbRow.ID,
116
+    page: officialRow.page,
117
+    row: officialRow.row,
118
+    officialCode: officialRow.officialCode,
119
+    schoolTarget: officialRow.schoolTarget,
120
+    schoolName: dbRow.SchoolFullName,
121
+    schoolTargetRemark: dbRow.SchoolTargetRemark ?? "",
122
+    PlanNum: dbRow.PlanNum,
123
+    totalOnly: scoreValue(officialRow, "score1") === null,
124
+    officialRemark: "",
125
+    ScoreTotal: scoreTotal,
126
+    Score1: scoreValue(officialRow, "score1"),
127
+    Score2: scoreValue(officialRow, "score2"),
128
+    Score3: scoreValue(officialRow, "score3"),
129
+    Score4: scoreValue(officialRow, "score4"),
130
+    ScoreTotalDifferenceValue: diffFromPrevious(scoreTotal, previousRow),
131
+    previousScoreTotal: previousRow ? decimalOrNull(previousRow.ScoreTotal) : null,
132
+  };
133
+}
134
+
135
+function assertExistingScoreCompatible(dbRow, update) {
136
+  for (const column of SCORE_COLUMNS) {
137
+    const oldValue = decimalOrNull(dbRow[column]);
138
+    const newValue = decimalOrNull(update[column]);
139
+    if (oldValue === null || oldValue === 0) {
140
+      continue;
141
+    }
142
+    if (newValue !== oldValue) {
143
+      throw new Error(`${dbRow.ID} ${dbRow.SchoolFullName} already has different ${column}: ${oldValue} -> ${newValue}`);
144
+    }
145
+  }
146
+}
147
+
148
+async function fetchRows(connection, year) {
149
+  const [rows] = await connection.query(
150
+    `
151
+      SELECT ID, ScoreYear, ScoreType, DistrictID, SchoolTarget, SchoolFullName,
152
+             SchoolTargetRemark, SchoolTargetRemark2, PlanNum,
153
+             ScoreTotal, Score1, Score2, Score3, Score4,
154
+             ScoreTotalDifferenceValue
155
+      FROM MPS_Score
156
+      WHERE ScoreYear = ?
157
+        AND ScoreType = ?
158
+        AND DistrictID = ?
159
+      ORDER BY ID
160
+    `,
161
+    [year, SCORE_TYPE, DISTRICT_ID],
162
+  );
163
+  return rows;
164
+}
165
+
166
+function collectUpdates(currentRows, previousRows) {
167
+  const currentByKey = new Map(currentRows.map((row) => [scoreKey(row), row]));
168
+  const previousByKey = new Map(previousRows.map((row) => [scoreKey(row), row]));
169
+  const updates = [];
170
+  const problems = [];
171
+
172
+  for (const officialRow of OFFICIAL_ROWS) {
173
+    const key = scoreKey(officialRow);
174
+    const dbRow = currentByKey.get(key);
175
+    if (!dbRow) {
176
+      problems.push({ type: "missing_current_row", officialRow });
177
+      continue;
178
+    }
179
+    if (dbRow.SchoolFullName !== officialRow.schoolName) {
180
+      problems.push({
181
+        type: "school_name_mismatch",
182
+        officialRow,
183
+        dbName: dbRow.SchoolFullName,
184
+      });
185
+      continue;
186
+    }
187
+    const update = buildUpdate(officialRow, dbRow, previousByKey.get(key));
188
+    assertExistingScoreCompatible(dbRow, update);
189
+    updates.push(update);
190
+  }
191
+
192
+  const officialKeys = new Set(OFFICIAL_ROWS.map(scoreKey));
193
+  const notInOfficialImage = currentRows
194
+    .filter((row) => !officialKeys.has(scoreKey(row)))
195
+    .map((row) => ({
196
+      ID: row.ID,
197
+      SchoolTarget: row.SchoolTarget,
198
+      SchoolFullName: row.SchoolFullName,
199
+      SchoolTargetRemark: row.SchoolTargetRemark ?? "",
200
+      PlanNum: row.PlanNum,
201
+      ScoreTotal: row.ScoreTotal,
202
+    }));
203
+
204
+  return { updates, problems, notInOfficialImage };
205
+}
206
+
207
+function summarize(updates, currentRows, notInOfficialImage) {
208
+  const scores = updates.map((row) => Number(row.ScoreTotal));
209
+  return {
210
+    district_id: DISTRICT_ID,
211
+    district: DISTRICT_NAME,
212
+    score_type: SCORE_TYPE,
213
+    official_rows: OFFICIAL_ROWS.length,
214
+    official_numeric_rows: updates.length,
215
+    official_no_admission_rows: 0,
216
+    official_total_only_rows: updates.filter((row) => row.totalOnly).length,
217
+    db_rows: currentRows.length,
218
+    not_in_official_image_rows: notInOfficialImage.length,
219
+    min_score: Math.min(...scores),
220
+    max_score: Math.max(...scores),
221
+  };
222
+}
223
+
224
+async function writeReport(report) {
225
+  await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2), "utf8");
226
+}
227
+
228
+async function writeBackup(currentRows) {
229
+  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "_");
230
+  const backupFile = path.join(SCRIPT_DIR, `mps_score_1_15_baoshan_scores_2026_backup_${stamp}.json`);
231
+  await fs.writeFile(backupFile, JSON.stringify(currentRows, null, 2), "utf8");
232
+  return backupFile;
233
+}
234
+
235
+async function updateScores(connection, updates) {
236
+  let changed = 0;
237
+  for (const update of updates) {
238
+    const [result] = await connection.execute(
239
+      `
240
+        UPDATE MPS_Score
241
+        SET ScoreTotal = ?,
242
+            Score1 = ?,
243
+            Score2 = ?,
244
+            Score3 = ?,
245
+            Score4 = ?,
246
+            ScoreTotalDifferenceValue = ?
247
+        WHERE ID = ?
248
+          AND ScoreYear = ?
249
+          AND ScoreType = ?
250
+          AND DistrictID = ?
251
+      `,
252
+      [
253
+        toFixed2(update.ScoreTotal),
254
+        toFixed2(update.Score1),
255
+        toFixed2(update.Score2),
256
+        toFixed2(update.Score3),
257
+        toFixed2(update.Score4),
258
+        toFixed2(update.ScoreTotalDifferenceValue),
259
+        update.id,
260
+        YEAR,
261
+        SCORE_TYPE,
262
+        DISTRICT_ID,
263
+      ],
264
+    );
265
+    if (result.affectedRows !== 1) {
266
+      throw new Error(`unexpected affectedRows for ID ${update.id}: ${result.affectedRows}`);
267
+    }
268
+    changed += result.changedRows;
269
+  }
270
+  return changed;
271
+}
272
+
273
+async function verify(connection, updates) {
274
+  const ids = updates.map((row) => row.id);
275
+  const [rows] = await connection.query(
276
+    `
277
+      SELECT ID, ScoreTotal, Score1, Score2, Score3, Score4, ScoreTotalDifferenceValue
278
+      FROM MPS_Score
279
+      WHERE ID IN (?)
280
+      ORDER BY ID
281
+    `,
282
+    [ids],
283
+  );
284
+  const actualById = new Map(rows.map((row) => [row.ID, row]));
285
+  const mismatches = [];
286
+  for (const update of updates) {
287
+    const actual = actualById.get(update.id);
288
+    if (!actual) {
289
+      mismatches.push({ id: update.id, column: "ID", actual: null, expected: update.id });
290
+      continue;
291
+    }
292
+    for (const column of SCORE_COLUMNS) {
293
+      const actualValue = toFixed2(actual[column]);
294
+      const expectedValue = toFixed2(update[column]);
295
+      if (actualValue !== expectedValue) {
296
+        mismatches.push({ id: update.id, column, actual: actualValue, expected: expectedValue });
297
+      }
298
+    }
299
+  }
300
+  if (mismatches.length) {
301
+    throw new Error(`verification failed: ${JSON.stringify(mismatches.slice(0, 10))}`);
302
+  }
303
+}
304
+
305
+async function fetchDistrictSummary(connection) {
306
+  const [rows] = await connection.query(
307
+    `
308
+      SELECT COUNT(*) AS db_rows,
309
+             SUM(PlanNum) AS plan_sum,
310
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) <> 0 THEN 1 ELSE 0 END) AS scored_rows,
311
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) = 0 THEN 1 ELSE 0 END) AS unscored_rows,
312
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) <> 0 AND Score1 IS NULL THEN 1 ELSE 0 END) AS total_only_rows,
313
+             MIN(NULLIF(ScoreTotal, 0)) AS min_score,
314
+             MAX(NULLIF(ScoreTotal, 0)) AS max_score
315
+      FROM MPS_Score
316
+      WHERE ScoreYear = ?
317
+        AND ScoreType = ?
318
+        AND DistrictID = ?
319
+    `,
320
+    [YEAR, SCORE_TYPE, DISTRICT_ID],
321
+  );
322
+  return rows[0];
323
+}
324
+
325
+async function main() {
326
+  const dryRun = process.argv.includes("--dry-run");
327
+  assertUniqueOfficialRows();
328
+
329
+  const connection = await mysql.createConnection({
330
+    ...config.database,
331
+    decimalNumbers: true,
332
+    connectTimeout: 10000,
333
+  });
334
+
335
+  try {
336
+    await connection.beginTransaction();
337
+    const currentRows = await fetchRows(connection, YEAR);
338
+    const previousRows = await fetchRows(connection, PREVIOUS_YEAR);
339
+    const { updates, problems, notInOfficialImage } = collectUpdates(currentRows, previousRows);
340
+    if (problems.length) {
341
+      throw new Error(`validation problems: ${JSON.stringify(problems, null, 2)}`);
342
+    }
343
+
344
+    const report = {
345
+      dry_run: dryRun,
346
+      source_pdf: "/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf",
347
+      rendered_pages: [
348
+        path.join(SCRIPT_DIR, "tmp/pdfs/baoshan/page-1.png"),
349
+        path.join(SCRIPT_DIR, "tmp/pdfs/baoshan/page-2.png"),
350
+      ],
351
+      summary: summarize(updates, currentRows, notInOfficialImage),
352
+      updates,
353
+      not_in_official_image: notInOfficialImage,
354
+    };
355
+    await writeReport(report);
356
+
357
+    console.log("summary", report.summary);
358
+    console.log("report", REPORT_FILE);
359
+    console.log("first_updates", updates.slice(0, 5));
360
+    console.log("not_in_official_image_count", notInOfficialImage.length);
361
+
362
+    if (dryRun) {
363
+      await connection.rollback();
364
+      console.log("dry_run_ok");
365
+      return;
366
+    }
367
+
368
+    const backupFile = await writeBackup(currentRows);
369
+    const changed = await updateScores(connection, updates);
370
+    await verify(connection, updates);
371
+    const districtSummary = await fetchDistrictSummary(connection);
372
+    await connection.commit();
373
+
374
+    console.log("updated_rows", updates.length);
375
+    console.log("changed_rows", changed);
376
+    console.log("backup", backupFile);
377
+    console.log("district_summary", districtSummary);
378
+  } catch (error) {
379
+    await connection.rollback();
380
+    throw error;
381
+  } finally {
382
+    await connection.end();
383
+  }
384
+}
385
+
386
+main().catch((error) => {
387
+  console.error(error);
388
+  process.exit(1);
389
+});

+ 382 - 0
秒过分数线数据导入/import_mps_score_1_15_batch_from_json_2026.js

@@ -0,0 +1,382 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const PREVIOUS_YEAR = "2025";
11
+const SCORE_TYPE = "1-15志愿";
12
+
13
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+const DEFAULT_INPUT_FILE = path.join(SCRIPT_DIR, "mps_score_1_15_batch_extracted_2026.json");
15
+const REPORT_FILE = path.join(SCRIPT_DIR, "mps_score_1_15_batch_import_report_2026.json");
16
+
17
+const SCORE_COLUMNS = [
18
+  "ScoreTotal",
19
+  "Score1",
20
+  "Score2",
21
+  "Score3",
22
+  "Score4",
23
+  "ScoreTotalDifferenceValue",
24
+];
25
+
26
+const DB_NAME_ALIASES = new Map([
27
+  ["上海市奉贤区奉城高级中学", "上海应用技术大学附属奉贤奉城高级中学"],
28
+  ["上海交通大学附属中学嘉定分校", "上海交通大学附属中学嘉定分校"],
29
+]);
30
+
31
+const decimalOrNull = (value) => (value === undefined || value === null || value === "" ? null : Number(value));
32
+const toFixed2 = (value) => (value === null || value === undefined ? null : Number(value).toFixed(2));
33
+const compact = (value) => String(value ?? "").replace(/\s+/g, "").trim();
34
+const stripOuterRemark = (name) => {
35
+  const match = String(name ?? "").match(/^(.*?)(([^()]+))$/);
36
+  return match ? { baseName: match[1], remark: match[2] } : null;
37
+};
38
+const dbKey = (row) => `${row.SchoolTarget}\t${row.SchoolTargetRemark ?? ""}`;
39
+
40
+function canonicalName(value) {
41
+  const compacted = compact(value)
42
+    .replace(/^上上海/, "上海")
43
+    .replace(/^海上海/, "上海");
44
+  return compact(DB_NAME_ALIASES.get(compacted) ?? compacted);
45
+}
46
+
47
+function scoreKey(row) {
48
+  return `${canonicalName(row.SchoolFullName)}\t${compact(row.SchoolTargetRemark ?? "")}`;
49
+}
50
+
51
+function rowCandidateKeys(officialRow) {
52
+  const rawName = compact(officialRow.schoolName);
53
+  const candidates = [
54
+    `${canonicalName(rawName)}\t${compact(officialRow.remark ?? "")}`,
55
+  ];
56
+
57
+  if (officialRow.officialCode === "074007" && canonicalName(rawName) === "上海市甘泉外国语中学") {
58
+    candidates.push(`${canonicalName(rawName)}\t(英语)`);
59
+  }
60
+
61
+  const split = stripOuterRemark(rawName);
62
+  if (split) {
63
+    candidates.push(`${canonicalName(split.baseName)}\t${compact(split.remark)}`);
64
+  }
65
+
66
+  candidates.push(`${canonicalName(rawName)}\t`);
67
+  return [...new Set(candidates)];
68
+}
69
+
70
+function displayOfficialRow(row) {
71
+  return {
72
+    source: row.source,
73
+    page: row.page,
74
+    row: row.row,
75
+    officialCode: row.officialCode,
76
+    schoolName: row.schoolName,
77
+    scoreTotal: row.scoreTotal,
78
+    score1: row.score1,
79
+    score2: row.score2,
80
+    score3: row.score3,
81
+    score4: row.score4,
82
+    remark: row.remark,
83
+    officialRemark: row.officialRemark,
84
+  };
85
+}
86
+
87
+function diffFromPrevious(currentScore, previousRow) {
88
+  if (!previousRow || previousRow.ScoreTotal === null || previousRow.ScoreTotal === undefined || previousRow.ScoreTotal === "") {
89
+    return null;
90
+  }
91
+  return Number((Number(currentScore) - Number(previousRow.ScoreTotal)).toFixed(2));
92
+}
93
+
94
+function assertExistingScoreCompatible(dbRow, update) {
95
+  for (const column of SCORE_COLUMNS) {
96
+    const oldValue = decimalOrNull(dbRow[column]);
97
+    const newValue = decimalOrNull(update[column]);
98
+    if (oldValue === null || oldValue === 0) {
99
+      continue;
100
+    }
101
+    if (newValue !== oldValue) {
102
+      throw new Error(`${dbRow.ID} ${dbRow.SchoolFullName} already has different ${column}: ${oldValue} -> ${newValue}`);
103
+    }
104
+  }
105
+}
106
+
107
+function buildNameMap(currentRows) {
108
+  const map = new Map();
109
+  for (const row of currentRows) {
110
+    const keys = [
111
+      scoreKey(row),
112
+      `${canonicalName(`${row.SchoolFullName}${row.SchoolTargetRemark ?? ""}`)}\t`,
113
+    ];
114
+    for (const key of new Set(keys)) {
115
+      const list = map.get(key) ?? [];
116
+      list.push(row);
117
+      map.set(key, list);
118
+    }
119
+  }
120
+  return map;
121
+}
122
+
123
+function findDbRow(officialRow, nameMap) {
124
+  for (const key of rowCandidateKeys(officialRow)) {
125
+    const candidates = nameMap.get(key) ?? [];
126
+    if (candidates.length === 1) {
127
+      return { dbRow: candidates[0], matchKey: key };
128
+    }
129
+    if (candidates.length > 1) {
130
+      return { problem: { type: "ambiguous_current_row", key, candidates: candidates.map((row) => ({
131
+        ID: row.ID,
132
+        SchoolTarget: row.SchoolTarget,
133
+        SchoolFullName: row.SchoolFullName,
134
+        SchoolTargetRemark: row.SchoolTargetRemark ?? "",
135
+      })) } };
136
+    }
137
+  }
138
+  return { problem: { type: "missing_current_row", officialRow: displayOfficialRow(officialRow), candidateKeys: rowCandidateKeys(officialRow) } };
139
+}
140
+
141
+function collectUpdates(district, currentRows, previousRows) {
142
+  const nameMap = buildNameMap(currentRows);
143
+  const previousByDbKey = new Map(previousRows.map((row) => [dbKey(row), row]));
144
+  const updates = [];
145
+  const problems = [];
146
+  const matchedIds = new Set();
147
+
148
+  for (const officialRow of district.rows) {
149
+    const { dbRow, matchKey, problem } = findDbRow(officialRow, nameMap);
150
+    if (problem) {
151
+      problems.push(problem);
152
+      continue;
153
+    }
154
+    if (matchedIds.has(dbRow.ID)) {
155
+      problems.push({ type: "duplicate_official_match", officialRow: displayOfficialRow(officialRow), dbID: dbRow.ID });
156
+      continue;
157
+    }
158
+
159
+    const update = {
160
+      id: dbRow.ID,
161
+      source: officialRow.source ?? district.source,
162
+      page: officialRow.page ?? null,
163
+      row: officialRow.row ?? null,
164
+      officialCode: officialRow.officialCode ?? "",
165
+      schoolTarget: dbRow.SchoolTarget,
166
+      schoolName: dbRow.SchoolFullName,
167
+      schoolTargetRemark: dbRow.SchoolTargetRemark ?? "",
168
+      PlanNum: dbRow.PlanNum,
169
+      officialSchoolName: officialRow.schoolName,
170
+      officialRemark: officialRow.officialRemark ?? "",
171
+      matchKey,
172
+      ScoreTotal: decimalOrNull(officialRow.scoreTotal) ?? 0,
173
+      Score1: decimalOrNull(officialRow.score1),
174
+      Score2: decimalOrNull(officialRow.score2),
175
+      Score3: decimalOrNull(officialRow.score3),
176
+      Score4: decimalOrNull(officialRow.score4),
177
+    };
178
+    update.ScoreTotalDifferenceValue = diffFromPrevious(update.ScoreTotal, previousByDbKey.get(dbKey(dbRow)));
179
+    update.previousScoreTotal = previousByDbKey.has(dbKey(dbRow)) ? decimalOrNull(previousByDbKey.get(dbKey(dbRow)).ScoreTotal) : null;
180
+
181
+    assertExistingScoreCompatible(dbRow, update);
182
+    matchedIds.add(dbRow.ID);
183
+    updates.push(update);
184
+  }
185
+
186
+  const notInSource = currentRows
187
+    .filter((row) => !matchedIds.has(row.ID))
188
+    .map((row) => ({
189
+      ID: row.ID,
190
+      SchoolTarget: row.SchoolTarget,
191
+      SchoolFullName: row.SchoolFullName,
192
+      SchoolTargetRemark: row.SchoolTargetRemark ?? "",
193
+      PlanNum: row.PlanNum,
194
+      ScoreTotal: row.ScoreTotal,
195
+    }));
196
+
197
+  return { updates, problems, notInSource };
198
+}
199
+
200
+async function fetchRows(connection, year, districtId) {
201
+  const [rows] = await connection.query(
202
+    `
203
+      SELECT ID, ScoreYear, ScoreType, DistrictID, SchoolTarget, SchoolFullName,
204
+             SchoolTargetRemark, SchoolTargetRemark2, PlanNum,
205
+             ScoreTotal, Score1, Score2, Score3, Score4,
206
+             ScoreTotalDifferenceValue
207
+      FROM MPS_Score
208
+      WHERE ScoreYear = ?
209
+        AND ScoreType = ?
210
+        AND DistrictID = ?
211
+      ORDER BY ID
212
+    `,
213
+    [year, SCORE_TYPE, districtId],
214
+  );
215
+  return rows;
216
+}
217
+
218
+function summarizeDistrict(district, updates, currentRows, notInSource, problems) {
219
+  const scoreRows = updates.filter((row) => Number(row.ScoreTotal) > 0);
220
+  const scores = scoreRows.map((row) => Number(row.ScoreTotal));
221
+  return {
222
+    district_id: district.districtId,
223
+    district: district.district,
224
+    source: district.source,
225
+    source_rows: district.rows.length,
226
+    matched_rows: updates.length,
227
+    db_rows: currentRows.length,
228
+    not_in_source_rows: notInSource.length,
229
+    problem_rows: problems.length,
230
+    scored_rows: scoreRows.length,
231
+    zero_score_rows: updates.length - scoreRows.length,
232
+    total_only_rows: updates.filter((row) => Number(row.ScoreTotal) > 0 && row.Score1 === null).length,
233
+    min_score: scores.length ? Math.min(...scores) : null,
234
+    max_score: scores.length ? Math.max(...scores) : null,
235
+  };
236
+}
237
+
238
+async function writeBackup(districtId, currentRows) {
239
+  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "_");
240
+  const backupFile = path.join(SCRIPT_DIR, `mps_score_1_15_batch_district_${districtId}_backup_${stamp}.json`);
241
+  await fs.writeFile(backupFile, JSON.stringify(currentRows, null, 2), "utf8");
242
+  return backupFile;
243
+}
244
+
245
+async function updateScores(connection, updates) {
246
+  let changed = 0;
247
+  for (const update of updates) {
248
+    const [result] = await connection.execute(
249
+      `
250
+        UPDATE MPS_Score
251
+        SET ScoreTotal = ?,
252
+            Score1 = ?,
253
+            Score2 = ?,
254
+            Score3 = ?,
255
+            Score4 = ?,
256
+            ScoreTotalDifferenceValue = ?
257
+        WHERE ID = ?
258
+          AND ScoreYear = ?
259
+          AND ScoreType = ?
260
+      `,
261
+      [
262
+        toFixed2(update.ScoreTotal),
263
+        toFixed2(update.Score1),
264
+        toFixed2(update.Score2),
265
+        toFixed2(update.Score3),
266
+        toFixed2(update.Score4),
267
+        toFixed2(update.ScoreTotalDifferenceValue),
268
+        update.id,
269
+        YEAR,
270
+        SCORE_TYPE,
271
+      ],
272
+    );
273
+    if (result.affectedRows !== 1) {
274
+      throw new Error(`unexpected affectedRows for ID ${update.id}: ${result.affectedRows}`);
275
+    }
276
+    changed += result.changedRows;
277
+  }
278
+  return changed;
279
+}
280
+
281
+async function verify(connection, updates) {
282
+  if (!updates.length) {
283
+    return;
284
+  }
285
+  const ids = updates.map((row) => row.id);
286
+  const [rows] = await connection.query(
287
+    `
288
+      SELECT ID, ScoreTotal, Score1, Score2, Score3, Score4, ScoreTotalDifferenceValue
289
+      FROM MPS_Score
290
+      WHERE ID IN (?)
291
+      ORDER BY ID
292
+    `,
293
+    [ids],
294
+  );
295
+  const actualById = new Map(rows.map((row) => [row.ID, row]));
296
+  const mismatches = [];
297
+  for (const update of updates) {
298
+    const actual = actualById.get(update.id);
299
+    if (!actual) {
300
+      mismatches.push({ id: update.id, column: "ID", actual: null, expected: update.id });
301
+      continue;
302
+    }
303
+    for (const column of SCORE_COLUMNS) {
304
+      const actualValue = toFixed2(actual[column]);
305
+      const expectedValue = toFixed2(update[column]);
306
+      if (actualValue !== expectedValue) {
307
+        mismatches.push({ id: update.id, column, actual: actualValue, expected: expectedValue });
308
+      }
309
+    }
310
+  }
311
+  if (mismatches.length) {
312
+    throw new Error(`verification failed: ${JSON.stringify(mismatches.slice(0, 20))}`);
313
+  }
314
+}
315
+
316
+async function main() {
317
+  const dryRun = process.argv.includes("--dry-run");
318
+  const inputArg = process.argv.find((arg) => arg.startsWith("--input="));
319
+  const inputFile = inputArg ? path.resolve(inputArg.slice("--input=".length)) : DEFAULT_INPUT_FILE;
320
+  const payload = JSON.parse(await fs.readFile(inputFile, "utf8"));
321
+
322
+  const connection = await mysql.createConnection({
323
+    ...config.database,
324
+    decimalNumbers: true,
325
+    connectTimeout: 10000,
326
+  });
327
+
328
+  const report = {
329
+    dry_run: dryRun,
330
+    input_file: inputFile,
331
+    generated_at: new Date().toISOString(),
332
+    summaries: [],
333
+    districts: [],
334
+  };
335
+
336
+  try {
337
+    await connection.beginTransaction();
338
+    for (const district of payload.districts) {
339
+      const currentRows = await fetchRows(connection, YEAR, district.districtId);
340
+      const previousRows = await fetchRows(connection, PREVIOUS_YEAR, district.districtId);
341
+      const { updates, problems, notInSource } = collectUpdates(district, currentRows, previousRows);
342
+      const summary = summarizeDistrict(district, updates, currentRows, notInSource, problems);
343
+      report.summaries.push(summary);
344
+      report.districts.push({ ...summary, updates, problems, not_in_source: notInSource });
345
+
346
+      console.log("summary", summary);
347
+      if (problems.length) {
348
+        console.log("problems", JSON.stringify(problems.slice(0, 10), null, 2));
349
+      }
350
+
351
+      if (!dryRun && updates.length) {
352
+        const backupFile = await writeBackup(district.districtId, currentRows);
353
+        const changedRows = await updateScores(connection, updates);
354
+        await verify(connection, updates);
355
+        summary.backup_file = backupFile;
356
+        summary.changed_rows = changedRows;
357
+      }
358
+    }
359
+
360
+    await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2), "utf8");
361
+    console.log("report", REPORT_FILE);
362
+
363
+    if (dryRun) {
364
+      await connection.rollback();
365
+      console.log("dry_run_ok");
366
+      return;
367
+    }
368
+
369
+    await connection.commit();
370
+    console.log("import_ok");
371
+  } catch (error) {
372
+    await connection.rollback();
373
+    throw error;
374
+  } finally {
375
+    await connection.end();
376
+  }
377
+}
378
+
379
+main().catch((error) => {
380
+  console.error(error);
381
+  process.exit(1);
382
+});

+ 435 - 0
秒过分数线数据导入/import_mps_score_1_15_pudong_scores_2026.js

@@ -0,0 +1,435 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const PREVIOUS_YEAR = "2025";
11
+const SCORE_TYPE = "1-15志愿";
12
+const DISTRICT_ID = 11;
13
+const DISTRICT_NAME = "浦东新区";
14
+
15
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
16
+const REPORT_FILE = path.join(SCRIPT_DIR, "mps_score_1_15_pudong_scores_2026_import_report.json");
17
+
18
+const SCORE_COLUMNS = [
19
+  "ScoreTotal",
20
+  "Score1",
21
+  "Score2",
22
+  "Score3",
23
+  "Score4",
24
+  "ScoreTotalDifferenceValue",
25
+];
26
+
27
+const OFFICIAL_ROWS = [
28
+  { page: 1, row: 1, schoolTarget: "1", schoolName: "上海市上海中学", scoreTotal: 715.5, score1: 425.5, score2: 149, score3: 133, score4: 144 },
29
+  { page: 1, row: 2, schoolTarget: "2", schoolName: "上海交通大学附属中学", scoreTotal: 714, score1: 427, score2: 149, score3: 134, score4: 144 },
30
+  { page: 1, row: 3, schoolTarget: "3", schoolName: "复旦大学附属中学", scoreTotal: 715, score1: 420, score2: 146, score3: 127, score4: 150 },
31
+  { page: 1, row: 4, schoolTarget: "4", schoolName: "华东师范大学第二附属中学", scoreTotal: 715, score1: 425.5, score2: 141, score3: 138, score4: 143 },
32
+  { page: 1, row: 5, schoolTarget: "6", schoolName: "上海市实验学校", scoreTotal: 709, score1: 416, score2: 139, score3: 128, score4: 145 },
33
+  { page: 1, row: 6, schoolTarget: "5", schoolName: "上海师范大学附属中学", scoreTotal: 701.5, score1: 419, score2: 146, score3: 130, score4: 141 },
34
+  { page: 1, row: 7, schoolTarget: "54", schoolName: "上海市建平中学", scoreTotal: 706, score1: 415, score2: 142, score3: 132, score4: 144 },
35
+  { page: 1, row: 8, schoolTarget: "55", schoolName: "上海市进才中学", scoreTotal: 703.5, score1: 412.5, score2: 136, score3: 131, score4: 142 },
36
+  { page: 1, row: 9, schoolTarget: "56", schoolName: "上海南汇中学", scoreTotal: 685, score1: 407, score2: 129, score3: 136, score4: 131 },
37
+  { page: 1, row: 10, schoolTarget: "57", schoolName: "上海市洋泾中学", scoreTotal: 697, score1: 413, score2: 142, score3: 124, score4: 141 },
38
+  { page: 1, row: 11, schoolTarget: "58", schoolName: "上海市川沙中学", scoreTotal: 695.5, score1: 408.5, score2: 139, score3: 128, score4: 147 },
39
+  { page: 1, row: 12, schoolTarget: "59", schoolName: "上海中学东校", scoreTotal: 700, score1: 414, score2: 136, score3: 132, score4: 139 },
40
+  { page: 1, row: 13, schoolTarget: "60", schoolName: "上海市浦东复旦附中分校", scoreTotal: 710, score1: 416, score2: 138, score3: 136, score4: 147 },
41
+  { page: 1, row: 14, schoolTarget: "196", schoolName: "上海市高桥中学", scoreTotal: 684, score1: 412, score2: 137, score3: 129, score4: 131 },
42
+  { page: 1, row: 15, schoolTarget: "192", schoolName: "华东师范大学附属东昌中学", scoreTotal: 686.5, score1: 400.5, score2: 130, score3: 126, score4: 139 },
43
+  { page: 1, row: 16, schoolTarget: "194", schoolName: "上海海事大学附属北蔡高级中学", scoreTotal: 674.5, score1: 388, score2: 135, score3: 119, score4: 142 },
44
+  { page: 1, row: 17, schoolTarget: "193", schoolName: "上海市香山中学", scoreTotal: 619.5, score1: 359, score2: 120, score3: 117, score4: 129 },
45
+  { page: 1, row: 18, schoolTarget: "855", schoolName: "上海市建平中学筠溪分校", scoreTotal: 690.5, score1: 401, score2: 132, score3: 127, score4: 144 },
46
+  { page: 1, row: 19, schoolTarget: "856", schoolName: "上海市进才中学根林分校", scoreTotal: 682, score1: 393, score2: 129, score3: 122, score4: 146 },
47
+  { page: 1, row: 20, schoolTarget: "232", schoolName: "上海市川沙中学友仁分校", scoreTotal: 678, score1: 394, score2: 132, score3: 118, score4: 144 },
48
+  { page: 1, row: 21, schoolTarget: "1029", schoolName: "上海市洋泾中学南校", scoreTotal: 672.5, score1: 389, score2: 119, score3: 132, score4: 143 },
49
+  { page: 1, row: 22, schoolTarget: "992", schoolName: "上海市浦东外国语学校东校", scoreTotal: 642, score1: 380, score2: 121, score3: 124, score4: 121 },
50
+  { page: 1, row: 23, schoolTarget: "195", schoolName: "上海市上南中学", scoreTotal: 659, score1: 395.5, score2: 127, score3: 127, score4: 125.5 },
51
+  { page: 1, row: 24, schoolTarget: "197", schoolName: "上海市三林中学", scoreTotal: 647.5, score1: 394.5, score2: 129, score3: 122, score4: 108 },
52
+  { page: 1, row: 25, schoolTarget: "198", schoolName: "上海市杨思高级中学", scoreTotal: 641, score1: 366, score2: 113, score3: 117, score4: 132 },
53
+  { page: 1, row: 26, schoolTarget: "199", schoolName: "上海市新川中学", scoreTotal: 658, score1: 386, score2: 130, score3: 123, score4: 130 },
54
+  { page: 1, row: 27, schoolTarget: "200", schoolName: "华东师范大学附属周浦中学", scoreTotal: 667, score1: 389, score2: 115, score3: 132, score4: 131 },
55
+  { page: 1, row: 28, schoolTarget: "201", schoolName: "上海海洋大学附属大团高级中学", scoreTotal: 610.5, score1: 359.5, score2: 98, score3: 124, score4: 118 },
56
+  { page: 1, row: 29, schoolTarget: "202", schoolName: "上海市新场中学", scoreTotal: 579, score1: 351, score2: 102, score3: 115, score4: 96 },
57
+  { page: 1, row: 30, schoolTarget: "857", schoolName: "华东理工大学附属浦东科技高级中学", scoreTotal: 665, score1: 394, score2: 132, score3: 125, score4: 129 },
58
+  { page: 1, row: 31, schoolTarget: "991", schoolName: "华东师范大学附属浦东临港高级中学", scoreTotal: 654.5, score1: 384, score2: 116, score3: 127, score4: 129 },
59
+  { page: 1, row: 32, schoolTarget: "1030", schoolName: "上海市浦东临港科技高级中学", scoreTotal: 656.5, score1: 384, score2: 123, score3: 123, score4: 133 },
60
+  { page: 1, row: 33, schoolTarget: "203", schoolName: "上海市文建中学", scoreTotal: 605.5, score1: 345, score2: 92, score3: 125, score4: 120 },
61
+  { page: 1, row: 34, schoolTarget: "204", schoolName: "上海市陆行中学", scoreTotal: 646.5, score1: 363, score2: 115, score3: 116, score4: 139 },
62
+  { page: 1, row: 35, schoolTarget: "205", schoolName: "上海市浦东中学", scoreTotal: 600, score1: 349.5, score2: 112, score3: 114, score4: 114 },
63
+  { page: 1, row: 36, schoolTarget: "206", schoolName: "上海立信会计金融学院附属高行中学", scoreTotal: 637.5, score1: 366.5, score2: 108, score3: 123, score4: 125 },
64
+  { page: 1, row: 37, schoolTarget: "207", schoolName: "上海市建平世纪中学", scoreTotal: 632.5, score1: 365, score2: 105, score3: 126, score4: 128 },
65
+  { page: 1, row: 38, schoolTarget: "208", schoolName: "上海交通大学附属中学浦东实验高中", scoreTotal: 614.5, score1: 351, score2: 109, score3: 110, score4: 122 },
66
+  { page: 1, row: 39, schoolTarget: "209", schoolName: "上海市南汇第一中学", scoreTotal: 627, score1: 364.5, score2: 114, score3: 117, score4: 129 },
67
+  { page: 1, row: 40, schoolTarget: "210", schoolName: "上海市泾南中学", scoreTotal: 581.5, score1: 353.5, score2: 103, score3: 117, score4: 103 },
68
+  { page: 1, row: 41, schoolTarget: "211", schoolName: "上海市沪新中学", scoreTotal: 560, score1: 322.5, score2: 118, score3: 104, score4: 108 },
69
+  { page: 1, row: 42, schoolTarget: "212", schoolName: "上海市育民中学", scoreTotal: 622, score1: 374.5, score2: 123, score3: 115, score4: 108 },
70
+  { page: 1, row: 43, schoolTarget: "213", schoolName: "上海第二工业大学附属龚路中学", scoreTotal: 590, score1: 336, score2: 91, score3: 116, score4: 118.5 },
71
+  { page: 1, row: 44, schoolTarget: "214", schoolName: "上海市江镇中学", scoreTotal: 584.5, score1: 342, score2: 94, score3: 124, score4: 108 },
72
+  { page: 1, row: 45, schoolTarget: "215", schoolName: "华东师范大学张江实验中学", scoreTotal: 627, score1: 360, score2: 97, score3: 119, score4: 127 },
73
+  { page: 1, row: 46, schoolTarget: "216", schoolName: "上海市三林中学东校", scoreTotal: 576, score1: 336, score2: 106, score3: 105, score4: 107 },
74
+  { page: 1, row: 47, schoolTarget: "221", schoolName: "上海市高东中学", scoreTotal: 567.5, score1: 328.5, score2: 94, score3: 118, score4: 99 },
75
+  { page: 1, row: 48, schoolTarget: "222", schoolName: "上海市祝桥高级中学", scoreTotal: 567.5, score1: 330.5, score2: 111, score3: 109, score4: 107 },
76
+  { page: 1, row: 49, schoolTarget: "224", schoolName: "上海市老港中学", scoreTotal: 543.5, score1: 319.5, score2: 98, score3: 114, score4: 90 },
77
+  { page: 1, row: 50, schoolTarget: "225", schoolName: "上海市泥城中学", scoreTotal: 534, score1: 285.5, score2: 107, score3: 118, score4: 112 },
78
+  { page: 1, row: 51, schoolTarget: "226", schoolName: "上海市吴迅中学", scoreTotal: 555.5, score1: 316.5, score2: 92, score3: 106, score4: 110 },
79
+  { page: 1, row: 52, schoolTarget: "1031", schoolName: "上海市浦东新区群峰高级中学", scoreTotal: 600, score1: 349, score2: 109, score3: 106, score4: 116 },
80
+
81
+  { page: 2, row: 53, schoolTarget: "227", schoolName: "上海市长岛中学", scoreTotal: 589, score1: 345.5, score2: 108, score3: 114, score4: 108 },
82
+  { page: 2, row: 54, schoolTarget: "235", schoolName: "上海民办光华中学", scoreTotal: 505.5, score1: 311, score2: 66, score3: 118, score4: 68, officialRemark: "未录满" },
83
+  { page: 2, row: 55, schoolTarget: "218", schoolName: "上海市浦东新区民办浦实高级中学", scoreTotal: 504, score1: 279, score2: 94, score3: 107, score4: 93, officialRemark: "未录满" },
84
+  { page: 2, row: 56, schoolTarget: "219", schoolName: "上海市民办育辛高级中学", scoreTotal: 501, score1: 272.5, score2: 91, score3: 104, score4: 105, officialRemark: "未录满" },
85
+  { page: 2, row: 57, schoolTarget: "220", schoolName: "上海市民办丰华高级中学", scoreTotal: 501.5, score1: 291, score2: 65, score3: 110, score4: 87, officialRemark: "未录满" },
86
+  { page: 2, row: 58, schoolTarget: "223", schoolName: "民办上海工商外国语职业学院附属中学", scoreTotal: 490.5, score1: 261, score2: 86, score3: 118, score4: 90, officialRemark: "未录满" },
87
+  { page: 2, row: 59, schoolTarget: "228", schoolName: "上海市民办金苹果学校", scoreTotal: 501, score1: 287, score2: 88, score3: 108, score4: 91, officialRemark: "未录满" },
88
+  { page: 2, row: 60, schoolTarget: "229", schoolName: "上海浦东新区民办东鼎外国语学校", scoreTotal: 480.5, score1: 262.5, score2: 57, score3: 105, score4: 92, officialRemark: "未录满" },
89
+  { page: 2, row: 61, schoolTarget: "230", schoolName: "上海市民办尚德实验学校", scoreTotal: 627, score1: 358.5, score2: 98, score3: 122, score4: 126 },
90
+  { page: 2, row: 62, schoolTarget: "858", schoolName: "上海浦东新区民办沪港学校", scoreTotal: 513.5, score1: 326.5, score2: 89, score3: 105, score4: 67.5, officialRemark: "未录满" },
91
+  { page: 2, row: 63, schoolTarget: "236", schoolName: "上海民办华曜浦东实验学校", scoreTotal: 672.5, score1: 394.5, score2: 125, score3: 123, score4: 138 },
92
+  { page: 2, row: 64, schoolTarget: "237", schoolName: "上海浦东新区民办万科学校", scoreTotal: 506, score1: 297.5, score2: 83, score3: 84, score4: 88, officialRemark: "未录满" },
93
+  { page: 2, row: 65, schoolTarget: "233", schoolName: "上海浦东新区民办惠立学校", scoreTotal: 502.5, score1: 305.5, score2: 73, score3: 113, score4: 72, officialRemark: "未录满" },
94
+  { page: 2, row: 66, schoolTarget: "238", schoolName: "上海浦东新区民办宏文学校", scoreTotal: 508, score1: 292.5, score2: 74, score3: 117, score4: 92, officialRemark: "未录满" },
95
+  { page: 2, row: 67, schoolTarget: "853", schoolName: "上海浦东民办未来科技学校", scoreTotal: 618.5, score1: 355, score2: 99, score3: 123, score4: 121 },
96
+  { page: 2, row: 68, schoolTarget: "234", schoolName: "上海浦东新区民办康德学校", scoreTotal: 518.5, score1: 296, score2: 68, score3: 125, score4: 93, officialRemark: "未录满" },
97
+  { page: 2, row: 69, schoolTarget: "854", schoolName: "上海浦东新区民办籽奥高级中学", scoreTotal: 510, score1: 299, score2: 86, score3: 101, score4: 85.5, officialRemark: "未录满" },
98
+  { page: 2, row: 70, schoolTarget: "81", schoolName: "上海市同济黄浦设计创意中学", scoreTotal: 642, score1: 370.5, score2: 120, score3: 125, score4: 137 },
99
+  { page: 2, row: 71, schoolTarget: "108", schoolName: "上海田家炳中学", scoreTotal: 652.5, score1: 378, score2: 124, score3: 116, score4: 135 },
100
+  { page: 2, row: 72, schoolTarget: "100", schoolName: "上海戏剧学院附属高级中学", scoreTotal: 626.5, score1: 361.5, score2: 103, score3: 124, score4: 123 },
101
+  { page: 2, row: 73, schoolTarget: "122", schoolName: "上海安生学校", scoreTotal: 608.5, score1: 365.5, score2: 111, score3: 115, score4: 109, officialRemark: "未录满" },
102
+  { page: 2, row: 74, schoolTarget: "117", schoolName: "上海音乐学院附属安师实验中学", scoreTotal: 597.5, score1: 343.5, score2: 91, score3: 119, score4: 115 },
103
+  { page: 2, row: 75, schoolTarget: "148", schoolName: "上海市文来中学", scoreTotal: 664.5, score1: 377.5, score2: 114, score3: 123, score4: 145 },
104
+  { page: 2, row: 76, schoolTarget: "153", schoolName: "上海市民办文绮中学", scoreTotal: 629, score1: 359, score2: 108, score3: 120, score4: 133 },
105
+  { page: 2, row: 77, schoolTarget: "156", schoolName: "上海市民办燎原双语高级中学", scoreTotal: 597, score1: 344.5, score2: 101, score3: 113, score4: 113 },
106
+  { page: 2, row: 78, schoolTarget: "851", schoolName: "上海民办行中中学", scoreTotal: 504.5, score1: 303.5, score2: 68, score3: 108, score4: 79.5 },
107
+  { page: 2, row: 79, schoolTarget: "177", schoolName: "上海存志高级中学", scoreTotal: 642, score1: 375.5, score2: 130, score3: 108, score4: 124 },
108
+  { page: 2, row: 80, schoolTarget: "179", schoolName: "上海创艺高级中学", scoreTotal: 489.5, score1: 283, score2: 74, score3: 103, score4: 82.5, officialRemark: "未录满" },
109
+  { page: 2, row: 81, schoolTarget: "180", schoolName: "上海市宝山华曜高级中学", scoreTotal: 646, score1: 380, score2: 118, score3: 124, score4: 134 },
110
+  { page: 2, row: 82, schoolTarget: "175", schoolName: "上海市同洲模范学校", scoreTotal: 520.5, score1: 287, score2: 84, score3: 119, score4: 96 },
111
+  { page: 2, row: 83, schoolTarget: "176", schoolName: "上海宝山区世外学校", scoreTotal: 684.5, score1: 399.5, score2: 130, score3: 126, score4: 141 },
112
+  { page: 2, row: 84, schoolTarget: "852", schoolName: "上海金瑞学校", scoreTotal: 535.5, score1: 317, score2: 102, score3: 98, score4: 92, officialRemark: "未录满" },
113
+  { page: 2, row: 85, schoolTarget: "178", schoolName: "上海宝山区民办维尚高级中学", noAdmission: true, officialRemark: "未录满" },
114
+  { page: 2, row: 86, schoolTarget: "190", schoolName: "上海嘉定区民办华盛怀少学校", scoreTotal: 522.5, score1: 316.5, score2: 83, score3: 108, score4: 83.5, officialRemark: "未录满" },
115
+  { page: 2, row: 87, schoolTarget: "189", schoolName: "上海华旭双语学校", noAdmission: true, officialRemark: "未录满" },
116
+  { page: 2, row: 88, schoolTarget: "243", schoolName: "上海市民办交大南洋中学", scoreTotal: 560.5, score1: 308, score2: 94, score3: 104, score4: 123 },
117
+  { page: 2, row: 89, schoolTarget: "244", schoolName: "上海金山区世外学校", scoreTotal: 639, score1: 390.5, score2: 116, score3: 128, score4: 114 },
118
+  { page: 2, row: 90, schoolTarget: "246", schoolName: "上海市民办永昌中学", noAdmission: true, officialRemark: "未录满" },
119
+  { page: 2, row: 91, schoolTarget: "859", schoolName: "上海市松江区科德高级中学", scoreTotal: 566, score1: 324, score2: 116, score3: 104, score4: 103, officialRemark: "未录满" },
120
+  { page: 2, row: 92, schoolTarget: "1034", schoolName: "上海市松江区励滕高级中学", noAdmission: true, officialRemark: "未录满" },
121
+  { page: 2, row: 93, schoolTarget: "860", schoolName: "上海领科双语学校", scoreTotal: 643.5, score1: 374, score2: 124, score3: 115, score4: 133 },
122
+  { page: 2, row: 94, schoolTarget: "251", schoolName: "上海市西外外国语学校", scoreTotal: 521, score1: 302.5, score2: 84, score3: 98, score4: 93 },
123
+  { page: 2, row: 95, schoolTarget: "254", schoolName: "上海赫贤学校", scoreTotal: 551.5, score1: 322.5, score2: 96, score3: 111, score4: 99.5, officialRemark: "未录满" },
124
+  { page: 2, row: 96, schoolTarget: "995", schoolName: "上海松江区爱菊学校", scoreTotal: 620.5, score1: 376, score2: 116, score3: 124, score4: 103 },
125
+  { page: 2, row: 97, schoolTarget: "863", schoolName: "上海青浦区世外高级中学", scoreTotal: 638, score1: 388.5, score2: 113, score3: 133, score4: 124.5 },
126
+  { page: 2, row: 98, schoolTarget: "862", schoolName: "上海青浦区宏润博源高级中学", scoreTotal: 613, score1: 360, score2: 106, score3: 113, score4: 117, officialRemark: "未录满" },
127
+  { page: 2, row: 99, schoolTarget: "259", schoolName: "上海宋庆龄学校", scoreTotal: 666, score1: 395, score2: 130, score3: 127, score4: 132 },
128
+  { page: 2, row: 100, schoolTarget: "260", schoolName: "上海青浦区协和双语学校", scoreTotal: 529, score1: 316, score2: 53, score3: 124, score4: 75, officialRemark: "未录满" },
129
+  { page: 2, row: 101, schoolTarget: "267", schoolName: "上海美达菲双语高级中学", scoreTotal: 511, score1: 276.5, score2: 91, score3: 106, score4: 105, officialRemark: "未录满" },
130
+  { page: 2, row: 102, schoolTarget: "266", schoolName: "上海奉贤区博华高级中学", scoreTotal: 556.5, score1: 317.5, score2: 111, score3: 110, score4: 107 },
131
+  { page: 2, row: 103, schoolTarget: "268", schoolName: "上海市崇明区城桥中学", scoreTotal: 560.5, score1: 309.5, score2: 93, score3: 122, score4: 113, officialRemark: "未录满" },
132
+  { page: 2, row: 104, schoolTarget: "272", schoolName: "上海民办民一中学", scoreTotal: 483.5, score1: 242.5, score2: 74, score3: 117, score4: 105 },
133
+  { page: 3, row: 105, schoolTarget: "271", schoolName: "上海市崇明区堡镇中学", scoreTotal: 559, score1: 308, score2: 108, score3: 117, score4: 120 },
134
+  { page: 3, row: 106, schoolTarget: "1039", schoolName: "上海新纪元双语学校", scoreTotal: 480.5, score1: 259.5, score2: 92, score3: 122, score4: 85 },
135
+];
136
+
137
+const decimalOrNull = (value) => (value === undefined || value === null || value === "" ? null : Number(value));
138
+const scoreValue = (row, column) => (row.noAdmission ? 0 : row[column]);
139
+const scoreKey = (row) => `${row.SchoolTarget ?? row.schoolTarget}\t${row.SchoolTargetRemark ?? row.remark ?? ""}`;
140
+const toFixed2 = (value) => (value === null || value === undefined ? null : Number(value).toFixed(2));
141
+
142
+function assertUniqueOfficialRows() {
143
+  const seen = new Set();
144
+  for (const row of OFFICIAL_ROWS) {
145
+    const key = scoreKey(row);
146
+    if (seen.has(key)) {
147
+      throw new Error(`duplicate official row key: ${key}`);
148
+    }
149
+    seen.add(key);
150
+  }
151
+}
152
+
153
+function diffFromPrevious(currentScore, previousRow) {
154
+  if (!previousRow || previousRow.ScoreTotal === null || previousRow.ScoreTotal === undefined || previousRow.ScoreTotal === "") {
155
+    return null;
156
+  }
157
+  return Number((Number(currentScore) - Number(previousRow.ScoreTotal)).toFixed(2));
158
+}
159
+
160
+function buildUpdate(officialRow, dbRow, previousRow) {
161
+  const scoreTotal = scoreValue(officialRow, "scoreTotal");
162
+  return {
163
+    id: dbRow.ID,
164
+    page: officialRow.page,
165
+    row: officialRow.row,
166
+    schoolTarget: officialRow.schoolTarget,
167
+    schoolName: dbRow.SchoolFullName,
168
+    schoolTargetRemark: dbRow.SchoolTargetRemark ?? "",
169
+    PlanNum: dbRow.PlanNum,
170
+    noAdmission: Boolean(officialRow.noAdmission),
171
+    officialRemark: officialRow.officialRemark ?? (officialRow.noAdmission ? "未录满" : ""),
172
+    ScoreTotal: scoreTotal,
173
+    Score1: scoreValue(officialRow, "score1"),
174
+    Score2: scoreValue(officialRow, "score2"),
175
+    Score3: scoreValue(officialRow, "score3"),
176
+    Score4: scoreValue(officialRow, "score4"),
177
+    ScoreTotalDifferenceValue: diffFromPrevious(scoreTotal, previousRow),
178
+    previousScoreTotal: previousRow ? decimalOrNull(previousRow.ScoreTotal) : null,
179
+  };
180
+}
181
+
182
+function assertExistingScoreCompatible(dbRow, update) {
183
+  for (const column of SCORE_COLUMNS) {
184
+    const oldValue = decimalOrNull(dbRow[column]);
185
+    const newValue = decimalOrNull(update[column]);
186
+    if (oldValue === null || oldValue === 0) {
187
+      continue;
188
+    }
189
+    if (newValue !== oldValue) {
190
+      throw new Error(`${dbRow.ID} ${dbRow.SchoolFullName} already has different ${column}: ${oldValue} -> ${newValue}`);
191
+    }
192
+  }
193
+}
194
+
195
+async function fetchRows(connection, year) {
196
+  const [rows] = await connection.query(
197
+    `
198
+      SELECT ID, ScoreYear, ScoreType, DistrictID, SchoolTarget, SchoolFullName,
199
+             SchoolTargetRemark, SchoolTargetRemark2, PlanNum,
200
+             ScoreTotal, Score1, Score2, Score3, Score4,
201
+             ScoreTotalDifferenceValue
202
+      FROM MPS_Score
203
+      WHERE ScoreYear = ?
204
+        AND ScoreType = ?
205
+        AND DistrictID = ?
206
+      ORDER BY ID
207
+    `,
208
+    [year, SCORE_TYPE, DISTRICT_ID],
209
+  );
210
+  return rows;
211
+}
212
+
213
+function collectUpdates(currentRows, previousRows) {
214
+  const currentByKey = new Map(currentRows.map((row) => [scoreKey(row), row]));
215
+  const previousByKey = new Map(previousRows.map((row) => [scoreKey(row), row]));
216
+  const updates = [];
217
+  const problems = [];
218
+
219
+  for (const officialRow of OFFICIAL_ROWS) {
220
+    const key = scoreKey(officialRow);
221
+    const dbRow = currentByKey.get(key);
222
+    if (!dbRow) {
223
+      problems.push({ type: "missing_current_row", officialRow });
224
+      continue;
225
+    }
226
+    if (dbRow.SchoolFullName !== officialRow.schoolName) {
227
+      problems.push({
228
+        type: "school_name_mismatch",
229
+        officialRow,
230
+        dbName: dbRow.SchoolFullName,
231
+      });
232
+      continue;
233
+    }
234
+    const update = buildUpdate(officialRow, dbRow, previousByKey.get(key));
235
+    assertExistingScoreCompatible(dbRow, update);
236
+    updates.push(update);
237
+  }
238
+
239
+  const officialKeys = new Set(OFFICIAL_ROWS.map(scoreKey));
240
+  const notInOfficialImage = currentRows
241
+    .filter((row) => !officialKeys.has(scoreKey(row)))
242
+    .map((row) => ({
243
+      ID: row.ID,
244
+      SchoolTarget: row.SchoolTarget,
245
+      SchoolFullName: row.SchoolFullName,
246
+      SchoolTargetRemark: row.SchoolTargetRemark ?? "",
247
+      PlanNum: row.PlanNum,
248
+      ScoreTotal: row.ScoreTotal,
249
+    }));
250
+
251
+  return { updates, problems, notInOfficialImage };
252
+}
253
+
254
+function summarize(updates, currentRows, notInOfficialImage) {
255
+  const numericUpdates = updates.filter((row) => !row.noAdmission);
256
+  const scores = numericUpdates.map((row) => Number(row.ScoreTotal));
257
+  return {
258
+    district_id: DISTRICT_ID,
259
+    district: DISTRICT_NAME,
260
+    score_type: SCORE_TYPE,
261
+    official_rows: OFFICIAL_ROWS.length,
262
+    official_numeric_rows: numericUpdates.length,
263
+    official_no_admission_rows: updates.length - numericUpdates.length,
264
+    db_rows: currentRows.length,
265
+    not_in_official_image_rows: notInOfficialImage.length,
266
+    min_score: Math.min(...scores),
267
+    max_score: Math.max(...scores),
268
+  };
269
+}
270
+
271
+async function writeReport(report) {
272
+  await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2), "utf8");
273
+}
274
+
275
+async function writeBackup(currentRows) {
276
+  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\\..+/, "").replace("T", "_");
277
+  const backupFile = path.join(SCRIPT_DIR, `mps_score_1_15_pudong_scores_2026_backup_${stamp}.json`);
278
+  await fs.writeFile(backupFile, JSON.stringify(currentRows, null, 2), "utf8");
279
+  return backupFile;
280
+}
281
+
282
+async function updateScores(connection, updates) {
283
+  let changed = 0;
284
+  for (const update of updates) {
285
+    const [result] = await connection.execute(
286
+      `
287
+        UPDATE MPS_Score
288
+        SET ScoreTotal = ?,
289
+            Score1 = ?,
290
+            Score2 = ?,
291
+            Score3 = ?,
292
+            Score4 = ?,
293
+            ScoreTotalDifferenceValue = ?
294
+        WHERE ID = ?
295
+          AND ScoreYear = ?
296
+          AND ScoreType = ?
297
+          AND DistrictID = ?
298
+      `,
299
+      [
300
+        toFixed2(update.ScoreTotal),
301
+        toFixed2(update.Score1),
302
+        toFixed2(update.Score2),
303
+        toFixed2(update.Score3),
304
+        toFixed2(update.Score4),
305
+        toFixed2(update.ScoreTotalDifferenceValue),
306
+        update.id,
307
+        YEAR,
308
+        SCORE_TYPE,
309
+        DISTRICT_ID,
310
+      ],
311
+    );
312
+    if (result.affectedRows !== 1) {
313
+      throw new Error(`unexpected affectedRows for ID ${update.id}: ${result.affectedRows}`);
314
+    }
315
+    changed += result.changedRows;
316
+  }
317
+  return changed;
318
+}
319
+
320
+async function verify(connection, updates) {
321
+  const ids = updates.map((row) => row.id);
322
+  const [rows] = await connection.query(
323
+    `
324
+      SELECT ID, ScoreTotal, Score1, Score2, Score3, Score4, ScoreTotalDifferenceValue
325
+      FROM MPS_Score
326
+      WHERE ID IN (?)
327
+      ORDER BY ID
328
+    `,
329
+    [ids],
330
+  );
331
+  const actualById = new Map(rows.map((row) => [row.ID, row]));
332
+  const mismatches = [];
333
+  for (const update of updates) {
334
+    const actual = actualById.get(update.id);
335
+    if (!actual) {
336
+      mismatches.push({ id: update.id, column: "ID", actual: null, expected: update.id });
337
+      continue;
338
+    }
339
+    for (const column of SCORE_COLUMNS) {
340
+      const actualValue = toFixed2(actual[column]);
341
+      const expectedValue = toFixed2(update[column]);
342
+      if (actualValue !== expectedValue) {
343
+        mismatches.push({ id: update.id, column, actual: actualValue, expected: expectedValue });
344
+      }
345
+    }
346
+  }
347
+  if (mismatches.length) {
348
+    throw new Error(`verification failed: ${JSON.stringify(mismatches.slice(0, 10))}`);
349
+  }
350
+}
351
+
352
+async function fetchDistrictSummary(connection) {
353
+  const [rows] = await connection.query(
354
+    `
355
+      SELECT COUNT(*) AS db_rows,
356
+             SUM(PlanNum) AS plan_sum,
357
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) <> 0 THEN 1 ELSE 0 END) AS scored_rows,
358
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) = 0 THEN 1 ELSE 0 END) AS unscored_rows,
359
+             MIN(NULLIF(ScoreTotal, 0)) AS min_score,
360
+             MAX(NULLIF(ScoreTotal, 0)) AS max_score
361
+      FROM MPS_Score
362
+      WHERE ScoreYear = ?
363
+        AND ScoreType = ?
364
+        AND DistrictID = ?
365
+    `,
366
+    [YEAR, SCORE_TYPE, DISTRICT_ID],
367
+  );
368
+  return rows[0];
369
+}
370
+
371
+async function main() {
372
+  const dryRun = process.argv.includes("--dry-run");
373
+  assertUniqueOfficialRows();
374
+
375
+  const connection = await mysql.createConnection({
376
+    ...config.database,
377
+    decimalNumbers: true,
378
+    connectTimeout: 10000,
379
+  });
380
+
381
+  try {
382
+    await connection.beginTransaction();
383
+    const currentRows = await fetchRows(connection, YEAR);
384
+    const previousRows = await fetchRows(connection, PREVIOUS_YEAR);
385
+    const { updates, problems, notInOfficialImage } = collectUpdates(currentRows, previousRows);
386
+    if (problems.length) {
387
+      throw new Error(`validation problems: ${JSON.stringify(problems, null, 2)}`);
388
+    }
389
+
390
+    const report = {
391
+      dry_run: dryRun,
392
+      source_images: [
393
+        "/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg",
394
+        "/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg",
395
+        "/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg",
396
+      ],
397
+      summary: summarize(updates, currentRows, notInOfficialImage),
398
+      updates,
399
+      not_in_official_image: notInOfficialImage,
400
+    };
401
+    await writeReport(report);
402
+
403
+    console.log("summary", report.summary);
404
+    console.log("report", REPORT_FILE);
405
+    console.log("first_updates", updates.slice(0, 5));
406
+    console.log("not_in_official_image_count", notInOfficialImage.length);
407
+
408
+    if (dryRun) {
409
+      await connection.rollback();
410
+      console.log("dry_run_ok");
411
+      return;
412
+    }
413
+
414
+    const backupFile = await writeBackup(currentRows);
415
+    const changed = await updateScores(connection, updates);
416
+    await verify(connection, updates);
417
+    const districtSummary = await fetchDistrictSummary(connection);
418
+    await connection.commit();
419
+
420
+    console.log("updated_rows", updates.length);
421
+    console.log("changed_rows", changed);
422
+    console.log("backup", backupFile);
423
+    console.log("district_summary", districtSummary);
424
+  } catch (error) {
425
+    await connection.rollback();
426
+    throw error;
427
+  } finally {
428
+    await connection.end();
429
+  }
430
+}
431
+
432
+main().catch((error) => {
433
+  console.error(error);
434
+  process.exit(1);
435
+});

+ 378 - 0
秒过分数线数据导入/import_mps_score_1_15_qingpu_scores_2026.js

@@ -0,0 +1,378 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const PREVIOUS_YEAR = "2025";
11
+const SCORE_TYPE = "1-15志愿";
12
+const DISTRICT_ID = 14;
13
+const DISTRICT_NAME = "青浦区";
14
+
15
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
16
+const REPORT_FILE = path.join(SCRIPT_DIR, "mps_score_1_15_qingpu_scores_2026_import_report.json");
17
+
18
+const SCORE_COLUMNS = [
19
+  "ScoreTotal",
20
+  "Score1",
21
+  "Score2",
22
+  "Score3",
23
+  "Score4",
24
+  "ScoreTotalDifferenceValue",
25
+];
26
+
27
+const OFFICIAL_ROWS = [
28
+  { page: 1, row: 1, schoolTarget: "1", schoolName: "上海市上海中学", scoreTotal: 718.5, score1: 426.5, score2: 147, score3: 137, score4: 144 },
29
+  { page: 1, row: 2, schoolTarget: "108", schoolName: "上海田家炳中学", scoreTotal: 639.0, score1: 366.0, score2: 120, score3: 116, score4: 131 },
30
+  { page: 1, row: 3, schoolTarget: "100", schoolName: "上海戏剧学院附属高级中学", remark: "(艺术班)", scoreTotal: 609.5, score1: 354.5, score2: 100, score3: 116, score4: 116 },
31
+  { page: 1, row: 4, schoolTarget: "122", schoolName: "上海安生学校", noAdmission: true, officialRemark: "未录满" },
32
+  { page: 1, row: 5, schoolTarget: "117", schoolName: "上海音乐学院附属安师实验中学", remark: "(艺术班)", scoreTotal: 631.0, score1: 375.0, score2: 116, score3: 120, score4: 112 },
33
+  { page: 1, row: 6, schoolTarget: "2", schoolName: "上海交通大学附属中学", scoreTotal: 716.5, score1: 422.5, score2: 148, score3: 127, score4: 145 },
34
+  { page: 1, row: 7, schoolTarget: "3", schoolName: "复旦大学附属中学", scoreTotal: 715.5, score1: 422.5, score2: 143, score3: 131, score4: 147 },
35
+  { page: 1, row: 8, schoolTarget: "156", schoolName: "上海市民办燎原双语高级中学", scoreTotal: 574.5, score1: 347.0, score2: 94, score3: 116, score4: 92 },
36
+  { page: 1, row: 9, schoolTarget: "1026", schoolName: "上海圣华紫竹高级中学", noAdmission: true, officialRemark: "未录满" },
37
+  { page: 1, row: 10, schoolTarget: "157", schoolName: "上海闵行区诺达双语学校", noAdmission: true, officialRemark: "未录满" },
38
+  { page: 1, row: 11, schoolTarget: "851", schoolName: "上海民办行中中学", scoreTotal: 480.0, score1: 253.0, score2: 94, score3: 109, score4: 97, officialRemark: "未录满" },
39
+  { page: 1, row: 12, schoolTarget: "177", schoolName: "上海存志高级中学", scoreTotal: 641.5, score1: 373.5, score2: 113, score3: 121, score4: 128 },
40
+  { page: 1, row: 13, schoolTarget: "179", schoolName: "上海创艺高级中学", scoreTotal: 586.0, score1: 347.5, score2: 89, score3: 130, score4: 99.5, officialRemark: "未录满" },
41
+  { page: 1, row: 14, schoolTarget: "180", schoolName: "上海市宝山华曜高级中学", scoreTotal: 514.0, score1: 299.0, score2: 90, score3: 91, score4: 81, officialRemark: "未录满" },
42
+  { page: 1, row: 15, schoolTarget: "175", schoolName: "上海市同洲模范学校", scoreTotal: 503.5, score1: 285.0, score2: 81, score3: 111, score4: 91, officialRemark: "未录满" },
43
+  { page: 1, row: 16, schoolTarget: "852", schoolName: "上海金瑞学校", noAdmission: true, officialRemark: "未录满" },
44
+  { page: 1, row: 17, schoolTarget: "176", schoolName: "上海宝山区世外学校", scoreTotal: 681.0, score1: 399.0, score2: 126, score3: 126, score4: 137 },
45
+  { page: 1, row: 18, schoolTarget: "188", schoolName: "上海市民办远东学校", scoreTotal: 529.0, score1: 298.0, score2: 109, score3: 99, score4: 97 },
46
+  { page: 1, row: 19, schoolTarget: "189", schoolName: "上海华旭双语学校", noAdmission: true, officialRemark: "未录满" },
47
+  { page: 1, row: 20, schoolTarget: "190", schoolName: "上海嘉定区民办华盛怀少学校", noAdmission: true, officialRemark: "未录满" },
48
+  { page: 1, row: 21, schoolTarget: "4", schoolName: "华东师范大学第二附属中学", scoreTotal: 715.0, score1: 424.0, score2: 143, score3: 135, score4: 146 },
49
+  { page: 1, row: 22, schoolTarget: "5", schoolName: "上海师范大学附属中学", scoreTotal: 695.5, score1: 404.5, score2: 135, score3: 128, score4: 143 },
50
+  { page: 1, row: 23, schoolTarget: "218", schoolName: "上海市浦东新区民办浦实高级中学", scoreTotal: 527.5, score1: 285.5, score2: 87, score3: 113, score4: 112, officialRemark: "未录满" },
51
+  { page: 1, row: 24, schoolTarget: "220", schoolName: "上海市民办丰华高级中学", scoreTotal: 538.0, score1: 272.0, score2: 87, score3: 99, score4: 126 },
52
+  { page: 1, row: 25, schoolTarget: "223", schoolName: "民办上海工商外国语职业学院附属中学", scoreTotal: 552.0, score1: 315.5, score2: 93, score3: 119, score4: 104 },
53
+  { page: 1, row: 26, schoolTarget: "229", schoolName: "上海浦东新区民办东鼎外国语学校", scoreTotal: 579.5, score1: 339.5, score2: 107, score3: 109, score4: 104 },
54
+  { page: 1, row: 27, schoolTarget: "230", schoolName: "上海市民办尚德实验学校", scoreTotal: 586.5, score1: 343.5, score2: 96, score3: 119, score4: 113 },
55
+  { page: 1, row: 28, schoolTarget: "243", schoolName: "上海市民办交大南洋中学", scoreTotal: 538.0, score1: 303.0, score2: 67, score3: 125, score4: 102 },
56
+  { page: 1, row: 29, schoolTarget: "246", schoolName: "上海市民办永昌中学", scoreTotal: 527.5, score1: 304.5, score2: 67, score3: 110, score4: 95, officialRemark: "未录满" },
57
+  { page: 1, row: 30, schoolTarget: "244", schoolName: "上海金山区世外学校", scoreTotal: 630.5, score1: 375.5, score2: 115, score3: 124, score4: 115 },
58
+  { page: 1, row: 31, schoolTarget: "859", schoolName: "上海市松江区科德高级中学", scoreTotal: 526.0, score1: 289.5, score2: 94, score3: 113, score4: 113, officialRemark: "未录满" },
59
+  { page: 1, row: 32, schoolTarget: "1034", schoolName: "上海市松江区励滕高级中学", noAdmission: true, officialRemark: "未录满" },
60
+  { page: 1, row: 33, schoolTarget: "255", schoolName: "上海市松江九峰实验学校", scoreTotal: 646.5, score1: 399.0, score2: 139, score3: 122, score4: 115 },
61
+  { page: 1, row: 34, schoolTarget: "860", schoolName: "上海领科双语学校", scoreTotal: 657.0, score1: 380.0, score2: 123, score3: 118, score4: 135, officialRemark: "未录满" },
62
+  { page: 1, row: 35, schoolTarget: "251", schoolName: "上海市西外外国语学校", scoreTotal: 501.0, score1: 274.0, score2: 75, score3: 86, score4: 96, officialRemark: "未录满" },
63
+  { page: 1, row: 36, schoolTarget: "254", schoolName: "上海赫贤学校", noAdmission: true, officialRemark: "未录满" },
64
+  { page: 1, row: 37, schoolTarget: "995", schoolName: "上海松江区爱菊学校", scoreTotal: 506.0, score1: 303.5, score2: 80, score3: 110, score4: 78, officialRemark: "未录满" },
65
+  { page: 1, row: 38, schoolTarget: "1036", schoolName: "上海师范大学附属青浦实验中学", scoreTotal: 626.5, score1: 360.5, score2: 119, score3: 125, score4: 127 },
66
+  { page: 1, row: 39, schoolTarget: "67", schoolName: "上海市青浦高级中学", scoreTotal: 671.5, score1: 395.0, score2: 133, score3: 118, score4: 131 },
67
+  { page: 1, row: 40, schoolTarget: "69", schoolName: "复旦大学附属中学青浦分校", scoreTotal: 695.0, score1: 412.0, score2: 137, score3: 129, score4: 138 },
68
+  { page: 1, row: 41, schoolTarget: "68", schoolName: "上海市朱家角中学", scoreTotal: 655.5, score1: 379.5, score2: 117, score3: 119, score4: 133 },
69
+  { page: 1, row: 42, schoolTarget: "257", schoolName: "上海市青浦区第二中学", scoreTotal: 555.5, score1: 322.5, score2: 93, score3: 106, score4: 100 },
70
+  { page: 1, row: 43, schoolTarget: "258", schoolName: "上海市青浦区东湖中学", scoreTotal: 599.0, score1: 365.0, score2: 112, score3: 123, score4: 92 },
71
+  { page: 1, row: 44, schoolTarget: "256", schoolName: "上海市青浦区第一中学", scoreTotal: 632.0, score1: 377.0, score2: 125, score3: 120, score4: 117 },
72
+  { page: 1, row: 45, schoolTarget: "863", schoolName: "上海青浦区世外高级中学", scoreTotal: 545.0, score1: 294.0, score2: 90, score3: 113, score4: 110, officialRemark: "未录满" },
73
+  { page: 1, row: 46, schoolTarget: "862", schoolName: "上海青浦区宏润博源高级中学", scoreTotal: 493.5, score1: 280.5, score2: 88, score3: 105, score4: 86, officialRemark: "未录满" },
74
+  { page: 1, row: 47, schoolTarget: "259", schoolName: "上海宋庆龄学校", scoreTotal: 614.5, score1: 358.5, score2: 101, score3: 112, score4: 118 },
75
+  { page: 1, row: 48, schoolTarget: "260", schoolName: "上海青浦区协和双语学校", scoreTotal: 530.5, score1: 304.5, score2: 95, score3: 102, score4: 94, officialRemark: "未录满" },
76
+  { page: 1, row: 49, schoolTarget: "267", schoolName: "上海美达菲双语高级中学", scoreTotal: 529.5, score1: 295.5, score2: 83, score3: 113, score4: 102, officialRemark: "未录满" },
77
+  { page: 1, row: 50, schoolTarget: "266", schoolName: "上海奉贤区博华高级中学", scoreTotal: 507.5, score1: 277.5, score2: 93, score3: 91, score4: 99 },
78
+  { page: 1, row: 51, schoolTarget: "268", schoolName: "上海市崇明区城桥中学", scoreTotal: 574.5, score1: 325.5, score2: 100, score3: 114, score4: 110 },
79
+  { page: 1, row: 52, schoolTarget: "271", schoolName: "上海市崇明区堡镇中学", scoreTotal: 579.0, score1: 323.0, score2: 98, score3: 119, score4: 116 },
80
+  { page: 1, row: 53, schoolTarget: "272", schoolName: "上海民办民一中学", scoreTotal: 451.5, score1: 227.5, score2: 105, score3: 88, score4: 95 },
81
+  { page: 1, row: 54, schoolTarget: "1039", schoolName: "上海新纪元双语学校", noAdmission: true, officialRemark: "未录满" },
82
+];
83
+
84
+const decimalOrNull = (value) => (value === undefined || value === null || value === "" ? null : Number(value));
85
+const scoreValue = (row, column) => (row.noAdmission ? 0 : row[column]);
86
+const scoreKey = (row) => `${row.SchoolTarget ?? row.schoolTarget}\t${row.SchoolTargetRemark ?? row.remark ?? ""}`;
87
+const toFixed2 = (value) => (value === null || value === undefined ? null : Number(value).toFixed(2));
88
+
89
+function assertUniqueOfficialRows() {
90
+  const seen = new Set();
91
+  for (const row of OFFICIAL_ROWS) {
92
+    const key = scoreKey(row);
93
+    if (seen.has(key)) {
94
+      throw new Error(`duplicate official row key: ${key}`);
95
+    }
96
+    seen.add(key);
97
+  }
98
+}
99
+
100
+function diffFromPrevious(currentScore, previousRow) {
101
+  if (!previousRow || previousRow.ScoreTotal === null || previousRow.ScoreTotal === undefined || previousRow.ScoreTotal === "") {
102
+    return null;
103
+  }
104
+  return Number((Number(currentScore) - Number(previousRow.ScoreTotal)).toFixed(2));
105
+}
106
+
107
+function buildUpdate(officialRow, dbRow, previousRow) {
108
+  const scoreTotal = scoreValue(officialRow, "scoreTotal");
109
+  return {
110
+    id: dbRow.ID,
111
+    page: officialRow.page,
112
+    row: officialRow.row,
113
+    schoolTarget: officialRow.schoolTarget,
114
+    schoolName: dbRow.SchoolFullName,
115
+    schoolTargetRemark: dbRow.SchoolTargetRemark ?? "",
116
+    PlanNum: dbRow.PlanNum,
117
+    noAdmission: Boolean(officialRow.noAdmission),
118
+    officialRemark: officialRow.officialRemark ?? (officialRow.noAdmission ? "未录满" : ""),
119
+    ScoreTotal: scoreTotal,
120
+    Score1: scoreValue(officialRow, "score1"),
121
+    Score2: scoreValue(officialRow, "score2"),
122
+    Score3: scoreValue(officialRow, "score3"),
123
+    Score4: scoreValue(officialRow, "score4"),
124
+    ScoreTotalDifferenceValue: diffFromPrevious(scoreTotal, previousRow),
125
+    previousScoreTotal: previousRow ? decimalOrNull(previousRow.ScoreTotal) : null,
126
+  };
127
+}
128
+
129
+function assertExistingScoreCompatible(dbRow, update) {
130
+  for (const column of SCORE_COLUMNS) {
131
+    const oldValue = decimalOrNull(dbRow[column]);
132
+    const newValue = decimalOrNull(update[column]);
133
+    if (oldValue === null || oldValue === 0) {
134
+      continue;
135
+    }
136
+    if (newValue !== oldValue) {
137
+      throw new Error(`${dbRow.ID} ${dbRow.SchoolFullName} already has different ${column}: ${oldValue} -> ${newValue}`);
138
+    }
139
+  }
140
+}
141
+
142
+async function fetchRows(connection, year) {
143
+  const [rows] = await connection.query(
144
+    `
145
+      SELECT ID, ScoreYear, ScoreType, DistrictID, SchoolTarget, SchoolFullName,
146
+             SchoolTargetRemark, SchoolTargetRemark2, PlanNum,
147
+             ScoreTotal, Score1, Score2, Score3, Score4,
148
+             ScoreTotalDifferenceValue
149
+      FROM MPS_Score
150
+      WHERE ScoreYear = ?
151
+        AND ScoreType = ?
152
+        AND DistrictID = ?
153
+      ORDER BY ID
154
+    `,
155
+    [year, SCORE_TYPE, DISTRICT_ID],
156
+  );
157
+  return rows;
158
+}
159
+
160
+function collectUpdates(currentRows, previousRows) {
161
+  const currentByKey = new Map(currentRows.map((row) => [scoreKey(row), row]));
162
+  const previousByKey = new Map(previousRows.map((row) => [scoreKey(row), row]));
163
+  const updates = [];
164
+  const problems = [];
165
+
166
+  for (const officialRow of OFFICIAL_ROWS) {
167
+    const key = scoreKey(officialRow);
168
+    const dbRow = currentByKey.get(key);
169
+    if (!dbRow) {
170
+      problems.push({ type: "missing_current_row", officialRow });
171
+      continue;
172
+    }
173
+    if (dbRow.SchoolFullName !== officialRow.schoolName) {
174
+      problems.push({
175
+        type: "school_name_mismatch",
176
+        officialRow,
177
+        dbName: dbRow.SchoolFullName,
178
+      });
179
+      continue;
180
+    }
181
+    const update = buildUpdate(officialRow, dbRow, previousByKey.get(key));
182
+    assertExistingScoreCompatible(dbRow, update);
183
+    updates.push(update);
184
+  }
185
+
186
+  const officialKeys = new Set(OFFICIAL_ROWS.map(scoreKey));
187
+  const notInOfficialImage = currentRows
188
+    .filter((row) => !officialKeys.has(scoreKey(row)))
189
+    .map((row) => ({
190
+      ID: row.ID,
191
+      SchoolTarget: row.SchoolTarget,
192
+      SchoolFullName: row.SchoolFullName,
193
+      SchoolTargetRemark: row.SchoolTargetRemark ?? "",
194
+      PlanNum: row.PlanNum,
195
+      ScoreTotal: row.ScoreTotal,
196
+    }));
197
+
198
+  return { updates, problems, notInOfficialImage };
199
+}
200
+
201
+function summarize(updates, currentRows, notInOfficialImage) {
202
+  const numericUpdates = updates.filter((row) => !row.noAdmission);
203
+  const scores = numericUpdates.map((row) => Number(row.ScoreTotal));
204
+  return {
205
+    district_id: DISTRICT_ID,
206
+    district: DISTRICT_NAME,
207
+    score_type: SCORE_TYPE,
208
+    official_rows: OFFICIAL_ROWS.length,
209
+    official_numeric_rows: numericUpdates.length,
210
+    official_no_admission_rows: updates.length - numericUpdates.length,
211
+    db_rows: currentRows.length,
212
+    not_in_official_image_rows: notInOfficialImage.length,
213
+    min_score: Math.min(...scores),
214
+    max_score: Math.max(...scores),
215
+  };
216
+}
217
+
218
+async function writeReport(report) {
219
+  await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2), "utf8");
220
+}
221
+
222
+async function writeBackup(currentRows) {
223
+  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "_");
224
+  const backupFile = path.join(SCRIPT_DIR, `mps_score_1_15_qingpu_scores_2026_backup_${stamp}.json`);
225
+  await fs.writeFile(backupFile, JSON.stringify(currentRows, null, 2), "utf8");
226
+  return backupFile;
227
+}
228
+
229
+async function updateScores(connection, updates) {
230
+  let changed = 0;
231
+  for (const update of updates) {
232
+    const [result] = await connection.execute(
233
+      `
234
+        UPDATE MPS_Score
235
+        SET ScoreTotal = ?,
236
+            Score1 = ?,
237
+            Score2 = ?,
238
+            Score3 = ?,
239
+            Score4 = ?,
240
+            ScoreTotalDifferenceValue = ?
241
+        WHERE ID = ?
242
+          AND ScoreYear = ?
243
+          AND ScoreType = ?
244
+          AND DistrictID = ?
245
+      `,
246
+      [
247
+        toFixed2(update.ScoreTotal),
248
+        toFixed2(update.Score1),
249
+        toFixed2(update.Score2),
250
+        toFixed2(update.Score3),
251
+        toFixed2(update.Score4),
252
+        toFixed2(update.ScoreTotalDifferenceValue),
253
+        update.id,
254
+        YEAR,
255
+        SCORE_TYPE,
256
+        DISTRICT_ID,
257
+      ],
258
+    );
259
+    if (result.affectedRows !== 1) {
260
+      throw new Error(`unexpected affectedRows for ID ${update.id}: ${result.affectedRows}`);
261
+    }
262
+    changed += result.changedRows;
263
+  }
264
+  return changed;
265
+}
266
+
267
+async function verify(connection, updates) {
268
+  const ids = updates.map((row) => row.id);
269
+  const [rows] = await connection.query(
270
+    `
271
+      SELECT ID, ScoreTotal, Score1, Score2, Score3, Score4, ScoreTotalDifferenceValue
272
+      FROM MPS_Score
273
+      WHERE ID IN (?)
274
+      ORDER BY ID
275
+    `,
276
+    [ids],
277
+  );
278
+  const actualById = new Map(rows.map((row) => [row.ID, row]));
279
+  const mismatches = [];
280
+  for (const update of updates) {
281
+    const actual = actualById.get(update.id);
282
+    if (!actual) {
283
+      mismatches.push({ id: update.id, column: "ID", actual: null, expected: update.id });
284
+      continue;
285
+    }
286
+    for (const column of SCORE_COLUMNS) {
287
+      const actualValue = toFixed2(actual[column]);
288
+      const expectedValue = toFixed2(update[column]);
289
+      if (actualValue !== expectedValue) {
290
+        mismatches.push({ id: update.id, column, actual: actualValue, expected: expectedValue });
291
+      }
292
+    }
293
+  }
294
+  if (mismatches.length) {
295
+    throw new Error(`verification failed: ${JSON.stringify(mismatches.slice(0, 10))}`);
296
+  }
297
+}
298
+
299
+async function fetchDistrictSummary(connection) {
300
+  const [rows] = await connection.query(
301
+    `
302
+      SELECT COUNT(*) AS db_rows,
303
+             SUM(PlanNum) AS plan_sum,
304
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) <> 0 THEN 1 ELSE 0 END) AS scored_rows,
305
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) = 0 THEN 1 ELSE 0 END) AS unscored_rows,
306
+             MIN(NULLIF(ScoreTotal, 0)) AS min_score,
307
+             MAX(NULLIF(ScoreTotal, 0)) AS max_score
308
+      FROM MPS_Score
309
+      WHERE ScoreYear = ?
310
+        AND ScoreType = ?
311
+        AND DistrictID = ?
312
+    `,
313
+    [YEAR, SCORE_TYPE, DISTRICT_ID],
314
+  );
315
+  return rows[0];
316
+}
317
+
318
+async function main() {
319
+  const dryRun = process.argv.includes("--dry-run");
320
+  assertUniqueOfficialRows();
321
+
322
+  const connection = await mysql.createConnection({
323
+    ...config.database,
324
+    decimalNumbers: true,
325
+    connectTimeout: 10000,
326
+  });
327
+
328
+  try {
329
+    await connection.beginTransaction();
330
+    const currentRows = await fetchRows(connection, YEAR);
331
+    const previousRows = await fetchRows(connection, PREVIOUS_YEAR);
332
+    const { updates, problems, notInOfficialImage } = collectUpdates(currentRows, previousRows);
333
+    if (problems.length) {
334
+      throw new Error(`validation problems: ${JSON.stringify(problems, null, 2)}`);
335
+    }
336
+
337
+    const report = {
338
+      dry_run: dryRun,
339
+      source_image: "/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/青浦区1.jpg",
340
+      summary: summarize(updates, currentRows, notInOfficialImage),
341
+      updates,
342
+      not_in_official_image: notInOfficialImage,
343
+    };
344
+    await writeReport(report);
345
+
346
+    console.log("summary", report.summary);
347
+    console.log("report", REPORT_FILE);
348
+    console.log("first_updates", updates.slice(0, 5));
349
+    console.log("not_in_official_image_count", notInOfficialImage.length);
350
+
351
+    if (dryRun) {
352
+      await connection.rollback();
353
+      console.log("dry_run_ok");
354
+      return;
355
+    }
356
+
357
+    const backupFile = await writeBackup(currentRows);
358
+    const changed = await updateScores(connection, updates);
359
+    await verify(connection, updates);
360
+    const districtSummary = await fetchDistrictSummary(connection);
361
+    await connection.commit();
362
+
363
+    console.log("updated_rows", updates.length);
364
+    console.log("changed_rows", changed);
365
+    console.log("backup", backupFile);
366
+    console.log("district_summary", districtSummary);
367
+  } catch (error) {
368
+    await connection.rollback();
369
+    throw error;
370
+  } finally {
371
+    await connection.end();
372
+  }
373
+}
374
+
375
+main().catch((error) => {
376
+  console.error(error);
377
+  process.exit(1);
378
+});

+ 396 - 0
秒过分数线数据导入/import_mps_score_1_15_xuhui_scores_2026.js

@@ -0,0 +1,396 @@
1
+import fs from "node:fs/promises";
2
+import path from "node:path";
3
+import { fileURLToPath } from "node:url";
4
+
5
+import mysql from "mysql2/promise";
6
+
7
+import config from "../src/config/index.js";
8
+
9
+const YEAR = "2026";
10
+const PREVIOUS_YEAR = "2025";
11
+const SCORE_TYPE = "1-15志愿";
12
+const DISTRICT_ID = 2;
13
+const DISTRICT_NAME = "徐汇区";
14
+
15
+const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
16
+const REPORT_FILE = path.join(SCRIPT_DIR, "mps_score_1_15_xuhui_scores_2026_import_report.json");
17
+
18
+const SCORE_COLUMNS = [
19
+  "ScoreTotal",
20
+  "Score1",
21
+  "Score2",
22
+  "Score3",
23
+  "Score4",
24
+  "ScoreTotalDifferenceValue",
25
+];
26
+
27
+const OFFICIAL_ROWS = [
28
+  { page: 1, row: 1, schoolTarget: "16", schoolName: "上海市第二中学", scoreTotal: 689.5, score1: 402.5, score2: 133, score3: 124, score4: 149 },
29
+  { page: 1, row: 2, schoolTarget: "20", schoolName: "上海市第二中学(梅陇校区)", scoreTotal: 697, score1: 407, score2: 137, score3: 124, score4: 145 },
30
+  { page: 1, row: 3, schoolTarget: "17", schoolName: "上海市南洋模范中学", scoreTotal: 705, score1: 411, score2: 142, score3: 126, score4: 149 },
31
+  { page: 1, row: 4, schoolTarget: "1", schoolName: "上海市上海中学", scoreTotal: 716, score1: 424, score2: 149, score3: 129, score4: 144 },
32
+  { page: 1, row: 5, schoolTarget: "18", schoolName: "上海市位育中学", scoreTotal: 700, score1: 406, score2: 134, score3: 126, score4: 146 },
33
+  { page: 1, row: 6, schoolTarget: "21", schoolName: "复旦大学附属中学徐汇分校", scoreTotal: 711, score1: 426, score2: 141, score3: 139, score4: 144 },
34
+  { page: 1, row: 7, schoolTarget: "19", schoolName: "上海市南洋中学", scoreTotal: 685.5, score1: 400.5, score2: 138, score3: 122, score4: 141 },
35
+  { page: 1, row: 8, schoolTarget: "87", schoolName: "上海市民办西南高级中学", scoreTotal: 559, score1: 314.5, score2: 115, score3: 119, score4: 107 },
36
+  { page: 1, row: 9, schoolTarget: "985", schoolName: "上海市位育附属徐汇科技实验中学", scoreTotal: 690, score1: 402, score2: 132, score3: 125, score4: 140 },
37
+  { page: 1, row: 10, schoolTarget: "83", schoolName: "上海市中国中学", scoreTotal: 663.5, score1: 387.5, score2: 115, score3: 127, score4: 133 },
38
+  { page: 1, row: 11, schoolTarget: "84", schoolName: "上海市第五十四中学", scoreTotal: 644, score1: 367.5, score2: 108, score3: 125, score4: 133 },
39
+  { page: 1, row: 12, schoolTarget: "82", schoolName: "上海市徐汇中学", scoreTotal: 674, score1: 399.5, score2: 129, score3: 127, score4: 130 },
40
+  { page: 1, row: 13, schoolTarget: "85", schoolName: "上海市第四中学", scoreTotal: 653.5, score1: 382.5, score2: 119, score3: 125, score4: 130 },
41
+  { page: 1, row: 14, schoolTarget: "88", schoolName: "上海市零陵中学", scoreTotal: 638, score1: 371.5, score2: 117, score3: 119, score4: 126 },
42
+  { page: 1, row: 15, schoolTarget: "89", schoolName: "华东理工大学附属中学", scoreTotal: 628.5, score1: 356.5, score2: 113, score3: 119, score4: 133 },
43
+  { page: 1, row: 16, schoolTarget: "86", schoolName: "上海市西南位育中学", scoreTotal: 671, score1: 395, score2: 126, score3: 131, score4: 135 },
44
+  { page: 1, row: 17, schoolTarget: "90", schoolName: "上海市西南模范中学", scoreTotal: 613.5, score1: 364.5, score2: 104, score3: 121, score4: 119 },
45
+  { page: 1, row: 18, schoolTarget: "1022", schoolName: "上海民办位育中学", scoreTotal: 658.5, score1: 384.5, score2: 117, score3: 122, score4: 133 },
46
+  { page: 1, row: 19, schoolTarget: "91", schoolName: "上海市紫竹园中学", scoreTotal: 607, score1: 344.5, score2: 109, score3: 118, score4: 122 },
47
+  { page: 1, row: 20, schoolTarget: "92", schoolName: "上海市徐汇区董恒甫高级中学", scoreTotal: 600, score1: 355, score2: 116, score3: 116, score4: 111 },
48
+
49
+  { page: 2, row: 1, schoolTarget: "2", schoolName: "上海交通大学附属中学", scoreTotal: 716, score1: 420, score2: 146, score3: 127, score4: 148 },
50
+  { page: 2, row: 2, schoolTarget: "3", schoolName: "复旦大学附属中学", scoreTotal: 715.5, score1: 422.5, score2: 141, score3: 134, score4: 147 },
51
+  { page: 2, row: 3, schoolTarget: "4", schoolName: "华东师范大学第二附属中学", scoreTotal: 715.5, score1: 423.5, score2: 146, score3: 131, score4: 146 },
52
+  { page: 2, row: 4, schoolTarget: "5", schoolName: "上海师范大学附属中学", scoreTotal: 701, score1: 414, score2: 134, score3: 133, score4: 143 },
53
+  { page: 2, row: 5, schoolTarget: "81", schoolName: "上海市同济黄浦设计创意中学", scoreTotal: 637, score1: 359, score2: 116, score3: 121, score4: 136 },
54
+  { page: 2, row: 6, schoolTarget: "105", schoolName: "上海市久隆模范中学", noAdmission: true },
55
+  { page: 2, row: 7, schoolTarget: "111", schoolName: "上海市民办扬波中学", scoreTotal: 609.5, score1: 354.5, score2: 133, score3: 118, score4: 120 },
56
+  { page: 2, row: 8, schoolTarget: "113", schoolName: "上海市民办风范中学", scoreTotal: 552.5, score1: 322.5, score2: 108, score3: 106, score4: 97 },
57
+  { page: 2, row: 9, schoolTarget: "100", schoolName: "上海戏剧学院附属高级中学", remark: "(艺术班)", scoreTotal: 615.5, score1: 355.5, score2: 117, score3: 108, score4: 118 },
58
+  { page: 2, row: 10, schoolTarget: "122", schoolName: "上海安生学校", scoreTotal: 539.5, score1: 299.5, score2: 71, score3: 117, score4: 107, officialRemark: "未录满" },
59
+  { page: 2, row: 11, schoolTarget: "117", schoolName: "上海音乐学院附属安师实验中学", remark: "(艺术班)", scoreTotal: 643.5, score1: 366.5, score2: 115, score3: 119, score4: 132, officialRemark: "未录满" },
60
+  { page: 2, row: 12, schoolTarget: "148", schoolName: "上海市文来中学", scoreTotal: 671.5, score1: 390.5, score2: 131, score3: 125, score4: 139 },
61
+  { page: 2, row: 13, schoolTarget: "156", schoolName: "上海市民办燎原双语高级中学", scoreTotal: 618.5, score1: 359, score2: 107, score3: 118, score4: 123 },
62
+  { page: 2, row: 14, schoolTarget: "157", schoolName: "上海闵行区诺达双语学校", scoreTotal: 544.5, score1: 317.5, score2: 96, score3: 122, score4: 94 },
63
+  { page: 2, row: 15, schoolTarget: "162", schoolName: "上海闵行区民办德闳学校", scoreTotal: 541.5, score1: 337.5, score2: 78, score3: 118, score4: 79 },
64
+  { page: 2, row: 16, schoolTarget: "851", schoolName: "上海民办行中中学", scoreTotal: 508.5, score1: 272, score2: 80, score3: 83, score4: 110 },
65
+  { page: 2, row: 17, schoolTarget: "177", schoolName: "上海存志高级中学", scoreTotal: 632.5, score1: 368.5, score2: 119, score3: 111, score4: 123 },
66
+  { page: 2, row: 18, schoolTarget: "178", schoolName: "上海宝山区民办维尚高级中学", scoreTotal: 543.5, score1: 325, score2: 81, score3: 116, score4: 91, officialRemark: "未录满" },
67
+  { page: 2, row: 19, schoolTarget: "179", schoolName: "上海创艺高级中学", scoreTotal: 581.5, score1: 347.5, score2: 101, score3: 118, score4: 101, officialRemark: "未录满" },
68
+  { page: 2, row: 20, schoolTarget: "180", schoolName: "上海市宝山华曜高级中学", scoreTotal: 639, score1: 377, score2: 126, score3: 122, score4: 126 },
69
+  { page: 2, row: 21, schoolTarget: "175", schoolName: "上海市同洲模范学校", scoreTotal: 558, score1: 317, score2: 78, score3: 110, score4: 105 },
70
+  { page: 2, row: 22, schoolTarget: "852", schoolName: "上海金瑞学校", scoreTotal: 529.5, score1: 309.5, score2: 62, score3: 108, score4: 92 },
71
+  { page: 2, row: 23, schoolTarget: "188", schoolName: "上海市民办远东学校", scoreTotal: 549, score1: 303, score2: 107, score3: 114, score4: 106 },
72
+  { page: 2, row: 24, schoolTarget: "189", schoolName: "上海华旭双语学校", scoreTotal: 493, score1: 274, score2: 89, score3: 106, score4: 96, officialRemark: "未录满" },
73
+  { page: 2, row: 25, schoolTarget: "190", schoolName: "上海嘉定区民办华盛怀少学校", noAdmission: true },
74
+
75
+  { page: 3, row: 1, schoolTarget: "218", schoolName: "上海市浦东新区民办浦实高级中学", scoreTotal: 620.5, score1: 367, score2: 104, score3: 126, score4: 109 },
76
+  { page: 3, row: 2, schoolTarget: "223", schoolName: "民办上海工商外国语职业学院附属中学", scoreTotal: 546.5, score1: 295.5, score2: 116, score3: 101, score4: 116 },
77
+  { page: 3, row: 3, schoolTarget: "228", schoolName: "上海市民办金苹果学校", scoreTotal: 545, score1: 319.5, score2: 88, score3: 102, score4: 107 },
78
+  { page: 3, row: 4, schoolTarget: "229", schoolName: "上海浦东新区民办东鼎外国语学校", scoreTotal: 576.5, score1: 329.5, score2: 100, score3: 102, score4: 116 },
79
+  { page: 3, row: 5, schoolTarget: "230", schoolName: "上海市民办尚德实验学校", scoreTotal: 631.5, score1: 371.5, score2: 114, score3: 129, score4: 116 },
80
+  { page: 3, row: 6, schoolTarget: "243", schoolName: "上海市民办交大南洋中学", scoreTotal: 573, score1: 311, score2: 87, score3: 110, score4: 128 },
81
+  { page: 3, row: 7, schoolTarget: "244", schoolName: "上海金山区世外学校", scoreTotal: 639.5, score1: 371.5, score2: 100, score3: 129, score4: 126 },
82
+  { page: 3, row: 8, schoolTarget: "859", schoolName: "上海市松江区科德高级中学", scoreTotal: 537.5, score1: 299, score2: 100, score3: 102, score4: 106 },
83
+  { page: 3, row: 9, schoolTarget: "1034", schoolName: "上海市松江区励滕高级中学", noAdmission: true },
84
+  { page: 3, row: 10, schoolTarget: "860", schoolName: "上海领科双语学校", scoreTotal: 646, score1: 375.5, score2: 125, score3: 119, score4: 135 },
85
+  { page: 3, row: 11, schoolTarget: "251", schoolName: "上海市西外外国语学校", scoreTotal: 563, score1: 330, score2: 101, score3: 120, score4: 97 },
86
+  { page: 3, row: 12, schoolTarget: "254", schoolName: "上海赫贤学校", scoreTotal: 528, score1: 289, score2: 72, score3: 105, score4: 108, officialRemark: "未录满" },
87
+  { page: 3, row: 13, schoolTarget: "995", schoolName: "上海松江区爱菊学校", scoreTotal: 610.5, score1: 353.5, score2: 98, score3: 119, score4: 118 },
88
+  { page: 3, row: 14, schoolTarget: "863", schoolName: "上海青浦区世外高级中学", scoreTotal: 649, score1: 383.5, score2: 120, score3: 120, score4: 133 },
89
+  { page: 3, row: 15, schoolTarget: "862", schoolName: "上海青浦区宏润博源高级中学", noAdmission: true },
90
+  { page: 3, row: 16, schoolTarget: "259", schoolName: "上海宋庆龄学校", scoreTotal: 580, score1: 339.5, score2: 85, score3: 119, score4: 109 },
91
+  { page: 3, row: 17, schoolTarget: "260", schoolName: "上海青浦区协和双语学校", scoreTotal: 611, score1: 350.5, score2: 101, score3: 111, score4: 117 },
92
+  { page: 3, row: 18, schoolTarget: "267", schoolName: "上海美达菲双语高级中学", noAdmission: true },
93
+  { page: 3, row: 19, schoolTarget: "266", schoolName: "上海奉贤区博华高级中学", scoreTotal: 540, score1: 292, score2: 97, score3: 108, score4: 108 },
94
+  { page: 3, row: 20, schoolTarget: "268", schoolName: "上海市崇明区城桥中学", scoreTotal: 513.5, score1: 291.5, score2: 88, score3: 106, score4: 98, officialRemark: "未录满" },
95
+  { page: 3, row: 21, schoolTarget: "271", schoolName: "上海市崇明区堡镇中学", scoreTotal: 548, score1: 303, score2: 95, score3: 115, score4: 107 },
96
+  { page: 3, row: 22, schoolTarget: "272", schoolName: "上海民办民一中学", scoreTotal: 472, score1: 254, score2: 49, score3: 109, score4: 91 },
97
+  { page: 3, row: 23, schoolTarget: "1039", schoolName: "上海新纪元双语学校", scoreTotal: 517.5, score1: 304.5, score2: 71, score3: 112, score4: 82, officialRemark: "未录满" },
98
+];
99
+
100
+const decimalOrNull = (value) => (value === undefined || value === null || value === "" ? null : Number(value));
101
+const scoreValue = (row, column) => (row.noAdmission ? 0 : row[column]);
102
+const scoreKey = (row) => `${row.SchoolTarget ?? row.schoolTarget}\t${row.SchoolTargetRemark ?? row.remark ?? ""}`;
103
+const toFixed2 = (value) => (value === null || value === undefined ? null : Number(value).toFixed(2));
104
+
105
+function assertUniqueOfficialRows() {
106
+  const seen = new Set();
107
+  for (const row of OFFICIAL_ROWS) {
108
+    const key = scoreKey(row);
109
+    if (seen.has(key)) {
110
+      throw new Error(`duplicate official row key: ${key}`);
111
+    }
112
+    seen.add(key);
113
+  }
114
+}
115
+
116
+function diffFromPrevious(currentScore, previousRow) {
117
+  if (!previousRow || previousRow.ScoreTotal === null || previousRow.ScoreTotal === undefined || previousRow.ScoreTotal === "") {
118
+    return null;
119
+  }
120
+  return Number((Number(currentScore) - Number(previousRow.ScoreTotal)).toFixed(2));
121
+}
122
+
123
+function buildUpdate(officialRow, dbRow, previousRow) {
124
+  const scoreTotal = scoreValue(officialRow, "scoreTotal");
125
+  return {
126
+    id: dbRow.ID,
127
+    page: officialRow.page,
128
+    row: officialRow.row,
129
+    schoolTarget: officialRow.schoolTarget,
130
+    schoolName: dbRow.SchoolFullName,
131
+    schoolTargetRemark: dbRow.SchoolTargetRemark ?? "",
132
+    PlanNum: dbRow.PlanNum,
133
+    noAdmission: Boolean(officialRow.noAdmission),
134
+    officialRemark: officialRow.officialRemark ?? (officialRow.noAdmission ? "线上无人投档" : ""),
135
+    ScoreTotal: scoreTotal,
136
+    Score1: scoreValue(officialRow, "score1"),
137
+    Score2: scoreValue(officialRow, "score2"),
138
+    Score3: scoreValue(officialRow, "score3"),
139
+    Score4: scoreValue(officialRow, "score4"),
140
+    ScoreTotalDifferenceValue: officialRow.noAdmission ? null : diffFromPrevious(scoreTotal, previousRow),
141
+    previousScoreTotal: previousRow ? decimalOrNull(previousRow.ScoreTotal) : null,
142
+  };
143
+}
144
+
145
+function assertExistingScoreCompatible(dbRow, update) {
146
+  for (const column of SCORE_COLUMNS) {
147
+    if (update.noAdmission && column === "ScoreTotalDifferenceValue") {
148
+      continue;
149
+    }
150
+    const oldValue = decimalOrNull(dbRow[column]);
151
+    const newValue = decimalOrNull(update[column]);
152
+    if (oldValue === null || oldValue === 0) {
153
+      continue;
154
+    }
155
+    if (newValue !== oldValue) {
156
+      throw new Error(`${dbRow.ID} ${dbRow.SchoolFullName} already has different ${column}: ${oldValue} -> ${newValue}`);
157
+    }
158
+  }
159
+}
160
+
161
+async function fetchRows(connection, year) {
162
+  const [rows] = await connection.query(
163
+    `
164
+      SELECT ID, ScoreYear, ScoreType, DistrictID, SchoolTarget, SchoolFullName,
165
+             SchoolTargetRemark, SchoolTargetRemark2, PlanNum,
166
+             ScoreTotal, Score1, Score2, Score3, Score4,
167
+             ScoreTotalDifferenceValue
168
+      FROM MPS_Score
169
+      WHERE ScoreYear = ?
170
+        AND ScoreType = ?
171
+        AND DistrictID = ?
172
+      ORDER BY ID
173
+    `,
174
+    [year, SCORE_TYPE, DISTRICT_ID],
175
+  );
176
+  return rows;
177
+}
178
+
179
+function collectUpdates(currentRows, previousRows) {
180
+  const currentByKey = new Map(currentRows.map((row) => [scoreKey(row), row]));
181
+  const previousByKey = new Map(previousRows.map((row) => [scoreKey(row), row]));
182
+  const updates = [];
183
+  const problems = [];
184
+
185
+  for (const officialRow of OFFICIAL_ROWS) {
186
+    const key = scoreKey(officialRow);
187
+    const dbRow = currentByKey.get(key);
188
+    if (!dbRow) {
189
+      problems.push({ type: "missing_current_row", officialRow });
190
+      continue;
191
+    }
192
+    if (dbRow.SchoolFullName !== officialRow.schoolName) {
193
+      problems.push({
194
+        type: "school_name_mismatch",
195
+        officialRow,
196
+        dbName: dbRow.SchoolFullName,
197
+      });
198
+      continue;
199
+    }
200
+    const update = buildUpdate(officialRow, dbRow, previousByKey.get(key));
201
+    assertExistingScoreCompatible(dbRow, update);
202
+    updates.push(update);
203
+  }
204
+
205
+  const officialKeys = new Set(OFFICIAL_ROWS.map(scoreKey));
206
+  const notInOfficialImage = currentRows
207
+    .filter((row) => !officialKeys.has(scoreKey(row)))
208
+    .map((row) => ({
209
+      ID: row.ID,
210
+      SchoolTarget: row.SchoolTarget,
211
+      SchoolFullName: row.SchoolFullName,
212
+      SchoolTargetRemark: row.SchoolTargetRemark ?? "",
213
+      PlanNum: row.PlanNum,
214
+      ScoreTotal: row.ScoreTotal,
215
+    }));
216
+
217
+  return { updates, problems, notInOfficialImage };
218
+}
219
+
220
+function summarize(updates, currentRows, notInOfficialImage) {
221
+  const numericUpdates = updates.filter((row) => !row.noAdmission);
222
+  const scores = numericUpdates.map((row) => Number(row.ScoreTotal));
223
+  return {
224
+    district_id: DISTRICT_ID,
225
+    district: DISTRICT_NAME,
226
+    score_type: SCORE_TYPE,
227
+    official_rows: OFFICIAL_ROWS.length,
228
+    official_numeric_rows: numericUpdates.length,
229
+    official_no_admission_rows: updates.length - numericUpdates.length,
230
+    db_rows: currentRows.length,
231
+    not_in_official_image_rows: notInOfficialImage.length,
232
+    min_score: Math.min(...scores),
233
+    max_score: Math.max(...scores),
234
+  };
235
+}
236
+
237
+async function writeReport(report) {
238
+  await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2), "utf8");
239
+}
240
+
241
+async function writeBackup(currentRows) {
242
+  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "_");
243
+  const backupFile = path.join(SCRIPT_DIR, `mps_score_1_15_xuhui_scores_2026_backup_${stamp}.json`);
244
+  await fs.writeFile(backupFile, JSON.stringify(currentRows, null, 2), "utf8");
245
+  return backupFile;
246
+}
247
+
248
+async function updateScores(connection, updates) {
249
+  let changed = 0;
250
+  for (const update of updates) {
251
+    const [result] = await connection.execute(
252
+      `
253
+        UPDATE MPS_Score
254
+        SET ScoreTotal = ?,
255
+            Score1 = ?,
256
+            Score2 = ?,
257
+            Score3 = ?,
258
+            Score4 = ?,
259
+            ScoreTotalDifferenceValue = ?
260
+        WHERE ID = ?
261
+          AND ScoreYear = ?
262
+          AND ScoreType = ?
263
+          AND DistrictID = ?
264
+      `,
265
+      [
266
+        toFixed2(update.ScoreTotal),
267
+        toFixed2(update.Score1),
268
+        toFixed2(update.Score2),
269
+        toFixed2(update.Score3),
270
+        toFixed2(update.Score4),
271
+        toFixed2(update.ScoreTotalDifferenceValue),
272
+        update.id,
273
+        YEAR,
274
+        SCORE_TYPE,
275
+        DISTRICT_ID,
276
+      ],
277
+    );
278
+    if (result.affectedRows !== 1) {
279
+      throw new Error(`unexpected affectedRows for ID ${update.id}: ${result.affectedRows}`);
280
+    }
281
+    changed += result.changedRows;
282
+  }
283
+  return changed;
284
+}
285
+
286
+async function verify(connection, updates) {
287
+  const ids = updates.map((row) => row.id);
288
+  const [rows] = await connection.query(
289
+    `
290
+      SELECT ID, ScoreTotal, Score1, Score2, Score3, Score4, ScoreTotalDifferenceValue
291
+      FROM MPS_Score
292
+      WHERE ID IN (?)
293
+      ORDER BY ID
294
+    `,
295
+    [ids],
296
+  );
297
+  const actualById = new Map(rows.map((row) => [row.ID, row]));
298
+  const mismatches = [];
299
+  for (const update of updates) {
300
+    const actual = actualById.get(update.id);
301
+    if (!actual) {
302
+      mismatches.push({ id: update.id, column: "ID", actual: null, expected: update.id });
303
+      continue;
304
+    }
305
+    for (const column of SCORE_COLUMNS) {
306
+      const actualValue = toFixed2(actual[column]);
307
+      const expectedValue = toFixed2(update[column]);
308
+      if (actualValue !== expectedValue) {
309
+        mismatches.push({ id: update.id, column, actual: actualValue, expected: expectedValue });
310
+      }
311
+    }
312
+  }
313
+  if (mismatches.length) {
314
+    throw new Error(`verification failed: ${JSON.stringify(mismatches.slice(0, 10))}`);
315
+  }
316
+}
317
+
318
+async function fetchDistrictSummary(connection) {
319
+  const [rows] = await connection.query(
320
+    `
321
+      SELECT COUNT(*) AS db_rows,
322
+             SUM(PlanNum) AS plan_sum,
323
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) <> 0 THEN 1 ELSE 0 END) AS scored_rows,
324
+             SUM(CASE WHEN COALESCE(ScoreTotal, 0) = 0 THEN 1 ELSE 0 END) AS unscored_rows,
325
+             MIN(NULLIF(ScoreTotal, 0)) AS min_score,
326
+             MAX(NULLIF(ScoreTotal, 0)) AS max_score
327
+      FROM MPS_Score
328
+      WHERE ScoreYear = ?
329
+        AND ScoreType = ?
330
+        AND DistrictID = ?
331
+    `,
332
+    [YEAR, SCORE_TYPE, DISTRICT_ID],
333
+  );
334
+  return rows[0];
335
+}
336
+
337
+async function main() {
338
+  const dryRun = process.argv.includes("--dry-run");
339
+  assertUniqueOfficialRows();
340
+
341
+  const connection = await mysql.createConnection({
342
+    ...config.database,
343
+    decimalNumbers: true,
344
+    connectTimeout: 10000,
345
+  });
346
+
347
+  try {
348
+    await connection.beginTransaction();
349
+    const currentRows = await fetchRows(connection, YEAR);
350
+    const previousRows = await fetchRows(connection, PREVIOUS_YEAR);
351
+    const { updates, problems, notInOfficialImage } = collectUpdates(currentRows, previousRows);
352
+    if (problems.length) {
353
+      throw new Error(`validation problems: ${JSON.stringify(problems, null, 2)}`);
354
+    }
355
+
356
+    const report = {
357
+      dry_run: dryRun,
358
+      summary: summarize(updates, currentRows, notInOfficialImage),
359
+      updates,
360
+      not_in_official_image: notInOfficialImage,
361
+    };
362
+    await writeReport(report);
363
+
364
+    console.log("summary", report.summary);
365
+    console.log("report", REPORT_FILE);
366
+    console.log("first_updates", updates.slice(0, 5));
367
+    console.log("not_in_official_image_count", notInOfficialImage.length);
368
+
369
+    if (dryRun) {
370
+      await connection.rollback();
371
+      console.log("dry_run_ok");
372
+      return;
373
+    }
374
+
375
+    const backupFile = await writeBackup(currentRows);
376
+    const changed = await updateScores(connection, updates);
377
+    await verify(connection, updates);
378
+    const districtSummary = await fetchDistrictSummary(connection);
379
+    await connection.commit();
380
+
381
+    console.log("updated_rows", updates.length);
382
+    console.log("changed_rows", changed);
383
+    console.log("backup", backupFile);
384
+    console.log("district_summary", districtSummary);
385
+  } catch (error) {
386
+    await connection.rollback();
387
+    throw error;
388
+  } finally {
389
+    await connection.end();
390
+  }
391
+}
392
+
393
+main().catch((error) => {
394
+  console.error(error);
395
+  process.exit(1);
396
+});

Разлика између датотеке није приказан због своје велике величине
+ 2996 - 0
秒过分数线数据导入/mps_score_1_15_2026_full_review_report.json


+ 78 - 0
秒过分数线数据导入/mps_score_1_15_2026_full_review_report.md

@@ -0,0 +1,78 @@
1
+# 2026 1-15志愿录入复查报告
2
+
3
+生成时间:2026-07-23T04:52:21.693Z
4
+
5
+## 最新处理
6
+- 徐汇区: 已按3页PDF重跑,68行全匹配,补录第3页23行;changed_rows=24
7
+- 长宁区: 57行全匹配;库内另有上海新纪元双语学校计划行,源PDF未列,按未投档保留0分;changed_rows=0
8
+- 黄浦区: 67行全匹配;changed_rows=0
9
+- 虹口区: 50行全匹配;changed_rows=0
10
+- 闵行区: 80行全匹配;PDF与图片版分数字段一致;changed_rows=0
11
+- 青浦区: 54行全匹配;当前HTML与数据库一致;changed_rows=0
12
+- 松江区: 计划图确认上海创艺高级中学有2个计划,已补入数据库 ID 40207;剩余源有库无2条在计划图未见。
13
+
14
+## 抽样审计
15
+- 数据库行数:961
16
+- 源报告逐字段比对行数:951
17
+- 分数字段不一致:0
18
+- 重复 key:0
19
+- 高风险抽样行:66
20
+- 明显异常候选:4
21
+
22
+## 人工视觉复核
23
+- 宝山区.pdf page 2: 确认上海民办民一中学为 483 / 311 / 70 / 110 / 59,综合测试 59 不是水印误读。
24
+- 崇明区1.jpeg: 确认上海民办民一中学为 441.5 / 228 / 40 / 100 / 82,上海新纪元双语学校为 464.5 / 236.5 / 67 / 98 / 94。
25
+- 浦东新区.pdf page 2-3: 确认低分段和未录满行,包括东鼎外国语 480.5 / 262.5 / 57 / 105 / 92,民一中学 483.5 / 242.5 / 74 / 117 / 105,新纪元 480.5 / 259.5 / 92 / 122 / 85。
26
+- 徐汇区.pdf page 3: 确认补录第 3 页 23 行,重点核对浦实 620.5、东鼎 576.5、励滕线上无人投档、民一 472、新纪元 517.5。
27
+- 青浦区.html: 确认当前 HTML 54 行全匹配数据库,宏润博源为 493.5,不是 0 分;正式核查 changed_rows=0。
28
+- 计划/1-15志愿/静安区2.png、杨浦区1-2.png、松江区2.png: 确认源有库无的7条中,只有松江上海创艺高级中学在计划图中出现;其余6条计划图未见。
29
+
30
+## 数据库汇总
31
+总行数:961;总计划数:64325
32
+
33
+|区|DB行|计划数|有分|0分|总分-only|最低|最高|
34
+|---|---|---|---|---|---|---|---|
35
+|黄浦区|67|1512|63|4|0|508.5|714.5|
36
+|徐汇区|68|3839|63|5|0|472|716|
37
+|长宁区|58|1617|50|8|42|477|715|
38
+|静安区|63|3250|57|6|0|490|713.5|
39
+|普陀区|68|3058|58|10|0|459|719.5|
40
+|虹口区|50|1947|44|6|0|512|716.5|
41
+|杨浦区|55|3243|46|9|0|451|715.5|
42
+|闵行区|80|7497|80|0|5|468|718|
43
+|宝山区|59|5322|59|0|17|462|717.5|
44
+|嘉定区|54|4051|47|7|33|492.5|718|
45
+|浦东新区|107|16629|102|5|0|480.5|715.5|
46
+|金山区|42|1506|29|13|0|452|717|
47
+|松江区|61|5016|56|5|0|488.5|717.5|
48
+|青浦区|54|2033|45|9|0|451.5|718.5|
49
+|奉贤区|46|2480|41|5|0|491|718|
50
+|崇明区|29|1325|20|9|0|441.5|711.5|
51
+
52
+## 需优先复查
53
+- 重复 key:0 条
54
+- 源文件/数据库不一致区块:6 个
55
+- 0分行:101 条
56
+- 0分但前年差非空非0:29 条
57
+- 总分-only 行:97 条
58
+
59
+## 源文件/数据库不一致
60
+说明:库有源无按“有计划但未投档”保留,不作为错误;重点看“源有库无”。
61
+
62
+- 长宁区: 源57,匹配57,库58,源有库无0,库有源无1
63
+- 静安区: 源65,匹配62,库63,源有库无3,库有源无1
64
+- 杨浦区: 源56,匹配55,库55,源有库无1,库有源无0
65
+- 浦东新区: 源106,匹配106,库107,源有库无0,库有源无1
66
+- 金山区: 源40,匹配40,库42,源有库无0,库有源无2
67
+- 松江区: 源58,匹配56,库61,源有库无2,库有源无5
68
+
69
+## 计划交叉核查
70
+- 静安区 上海宋庆龄学校: 计划图未见;分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。
71
+- 静安区 上海奉贤区博华高级中学: 计划图未见;分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。
72
+- 静安区 上海新纪元双语学校: 计划图未见;分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。
73
+- 杨浦区 复旦大学第二附属中学: 计划图未见;分数线源有,本区/外区计划图均未见该校;不补库,留作源文件/计划文件不一致复查。
74
+- 松江区 上海创艺高级中学: 计划图有,计划数2;分数线源有,计划图也有 2 个计划;数据库缺少计划行,本脚本补入。
75
+- 松江区 上海宝山区世外学校: 计划图未见;分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。
76
+- 松江区 上海市民办远东学校: 计划图未见;分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。
77
+
78
+详细清单见 `mps_score_1_15_2026_full_review_report.json`。

+ 180 - 0
秒过分数线数据导入/mps_score_1_15_2026_plan_crosscheck_report.json

@@ -0,0 +1,180 @@
1
+{
2
+  "generated_at": "2026-07-23T04:46:23.067Z",
3
+  "scope": "2026 1-15志愿 源有分数线但数据库无计划行的交叉核查",
4
+  "plan_directory": "/Volumes/程杰外接SD盘/上海中考招生计划/2026/计划/1-15志愿",
5
+  "rule": "有计划但分数线没见,视为正常未投档;分数线有但库无计划,先查计划图,有计划才补库。",
6
+  "checked_rows": [
7
+    {
8
+      "districtId": 4,
9
+      "district": "静安区",
10
+      "schoolName": "上海宋庆龄学校",
11
+      "officialSource": "静安区-外区.pdf page 2 row 13",
12
+      "score": {
13
+        "total": 627,
14
+        "score1": 364,
15
+        "score2": 108,
16
+        "score3": 121,
17
+        "score4": 122
18
+      },
19
+      "planSource": "静安区2.png",
20
+      "planFound": false,
21
+      "conclusion": "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。"
22
+    },
23
+    {
24
+      "districtId": 4,
25
+      "district": "静安区",
26
+      "schoolName": "上海奉贤区博华高级中学",
27
+      "officialSource": "静安区-外区.pdf page 2 row 16",
28
+      "score": {
29
+        "total": 556,
30
+        "score1": 328,
31
+        "score2": 105,
32
+        "score3": 103,
33
+        "score4": 97
34
+      },
35
+      "planSource": "静安区2.png",
36
+      "planFound": false,
37
+      "conclusion": "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。"
38
+    },
39
+    {
40
+      "districtId": 4,
41
+      "district": "静安区",
42
+      "schoolName": "上海新纪元双语学校",
43
+      "officialSource": "静安区-外区.pdf page 2 row 18",
44
+      "score": {
45
+        "total": 483,
46
+        "score1": 271.5,
47
+        "score2": 100,
48
+        "score3": 89,
49
+        "score4": 83
50
+      },
51
+      "planSource": "静安区2.png",
52
+      "planFound": false,
53
+      "conclusion": "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。"
54
+    },
55
+    {
56
+      "districtId": 7,
57
+      "district": "杨浦区",
58
+      "schoolName": "复旦大学第二附属中学",
59
+      "officialSource": "杨浦区.html row 8",
60
+      "score": {
61
+        "total": 696,
62
+        "score1": 406,
63
+        "score2": 133,
64
+        "score3": 134,
65
+        "score4": 145
66
+      },
67
+      "planSource": "杨浦区1.png + 杨浦区2.png",
68
+      "planFound": false,
69
+      "conclusion": "分数线源有,本区/外区计划图均未见该校;不补库,留作源文件/计划文件不一致复查。"
70
+    },
71
+    {
72
+      "districtId": 13,
73
+      "district": "松江区",
74
+      "schoolName": "上海创艺高级中学",
75
+      "officialCode": "134014",
76
+      "officialSource": "松江区.mhtml row 16",
77
+      "score": {
78
+        "total": 510.5,
79
+        "score1": 289.5,
80
+        "score2": 84,
81
+        "score3": 111,
82
+        "score4": 89
83
+      },
84
+      "planSource": "松江区2.png",
85
+      "planFound": true,
86
+      "planNum": 2,
87
+      "schoolTarget": "179",
88
+      "conclusion": "分数线源有,计划图也有 2 个计划;数据库缺少计划行,本脚本补入。"
89
+    },
90
+    {
91
+      "districtId": 13,
92
+      "district": "松江区",
93
+      "schoolName": "上海宝山区世外学校",
94
+      "officialCode": "135034",
95
+      "officialSource": "松江区.mhtml row 20",
96
+      "score": {
97
+        "total": 684.5,
98
+        "score1": 399,
99
+        "score2": 142,
100
+        "score3": 119,
101
+        "score4": 142
102
+      },
103
+      "planSource": "松江区2.png",
104
+      "planFound": false,
105
+      "conclusion": "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。"
106
+    },
107
+    {
108
+      "districtId": 13,
109
+      "district": "松江区",
110
+      "schoolName": "上海市民办远东学校",
111
+      "officialCode": "145007",
112
+      "officialSource": "松江区.mhtml row 21",
113
+      "score": {
114
+        "total": 579,
115
+        "score1": 331.5,
116
+        "score2": 99,
117
+        "score3": 116,
118
+        "score4": 110
119
+      },
120
+      "planSource": "松江区2.png",
121
+      "planFound": false,
122
+      "conclusion": "分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。"
123
+    }
124
+  ],
125
+  "db_actions": [
126
+    {
127
+      "action": "inserted",
128
+      "insertedId": 40207,
129
+      "row": {
130
+        "districtId": 13,
131
+        "district": "松江区",
132
+        "schoolName": "上海创艺高级中学",
133
+        "officialCode": "134014",
134
+        "officialSource": "松江区.mhtml row 16",
135
+        "score": {
136
+          "total": 510.5,
137
+          "score1": 289.5,
138
+          "score2": 84,
139
+          "score3": 111,
140
+          "score4": 89
141
+        },
142
+        "planSource": "松江区2.png",
143
+        "planFound": true,
144
+        "planNum": 2,
145
+        "schoolTarget": "179",
146
+        "conclusion": "分数线源有,计划图也有 2 个计划;数据库缺少计划行,本脚本补入。"
147
+      },
148
+      "previous": {
149
+        "previousPlan": 7,
150
+        "previousScore": 0,
151
+        "planDiff": -5,
152
+        "scoreDiff": 510.5
153
+      },
154
+      "inserted": {
155
+        "ID": 40207,
156
+        "ScoreYear": "2026",
157
+        "ScoreType": "1-15志愿",
158
+        "DistrictID": 13,
159
+        "SchoolOfGraduation": null,
160
+        "SchoolFullNameJunior": null,
161
+        "SchoolTarget": "179",
162
+        "SchoolFullName": "上海创艺高级中学",
163
+        "SchoolTargetRemark": "",
164
+        "PlanNum": 2,
165
+        "ScoreTotal": "510.50",
166
+        "Score1": "289.50",
167
+        "Score2": "84.00",
168
+        "Score3": "111.00",
169
+        "Score4": "89.00",
170
+        "SchoolTargetRemark2": null,
171
+        "PlanNumDifferenceValue": -5,
172
+        "ScoreTotalDifferenceValue": "510.50",
173
+        "OrderID": 0,
174
+        "SchoolNumber": "",
175
+        "SchoolNumber2": "",
176
+        "SchoolOfGraduation1": "0"
177
+      }
178
+    }
179
+  ]
180
+}

+ 24 - 0
秒过分数线数据导入/mps_score_1_15_2026_plan_crosscheck_report.md

@@ -0,0 +1,24 @@
1
+# 2026 1-15志愿计划交叉核查报告
2
+
3
+生成时间:2026-07-23T04:46:23.067Z
4
+
5
+计划目录:/Volumes/程杰外接SD盘/上海中考招生计划/2026/计划/1-15志愿
6
+
7
+规则:有计划但分数线没见,按未投档处理;分数线有但数据库无计划行,先查计划图,计划图有才补库。
8
+
9
+## 核查结论
10
+
11
+|区|学校|分数线源位置|计划图|计划图是否有|处理|
12
+|---|---|---|---|---|---|
13
+|静安区|上海宋庆龄学校|静安区-外区.pdf page 2 row 13|静安区2.png|未见|分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。|
14
+|静安区|上海奉贤区博华高级中学|静安区-外区.pdf page 2 row 16|静安区2.png|未见|分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。|
15
+|静安区|上海新纪元双语学校|静安区-外区.pdf page 2 row 18|静安区2.png|未见|分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。|
16
+|杨浦区|复旦大学第二附属中学|杨浦区.html row 8|杨浦区1.png + 杨浦区2.png|未见|分数线源有,本区/外区计划图均未见该校;不补库,留作源文件/计划文件不一致复查。|
17
+|松江区|上海创艺高级中学|松江区.mhtml row 16|松江区2.png|有,计划数 2|分数线源有,计划图也有 2 个计划;数据库缺少计划行,本脚本补入。|
18
+|松江区|上海宝山区世外学校|松江区.mhtml row 20|松江区2.png|未见|分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。|
19
+|松江区|上海市民办远东学校|松江区.mhtml row 21|松江区2.png|未见|分数线源有,计划图未见该校;不补库,留作源文件/计划文件不一致复查。|
20
+
21
+## 数据库动作
22
+
23
+- 已补入 松江区 上海创艺高级中学: ID 40207, SchoolTarget 179, PlanNum 2, ScoreTotal 510.50, Score1 289.50, Score2 84.00, Score3 111.00, Score4 89.00, PlanDiff -5, ScoreDiff 510.50
24
+

Разлика између датотеке није приказан због своје велике величине
+ 4000 - 0
秒过分数线数据导入/mps_score_1_15_2026_sample_audit_report.json


+ 93 - 0
秒过分数线数据导入/mps_score_1_15_2026_sample_audit_report.md

@@ -0,0 +1,93 @@
1
+# 2026 1-15志愿抽样复核报告
2
+
3
+生成时间:2026-07-23T04:52:21.454Z
4
+
5
+## 机器一致性
6
+- 数据库行数:961
7
+- 源报告逐字段比对行数:951
8
+- 分数字段不一致:0
9
+- 重复 key:0
10
+- 宽松范围异常:4
11
+- 抽样行:142
12
+- 高风险抽样行:66
13
+
14
+## 高风险样本
15
+|区|原因|学校|源|页/行|总分|语数外|数学|语文|综合|备注|
16
+|---|---|---|---|---|---|---|---|---|---|---|
17
+|徐汇区|min_score|上海民办民一中学|mps_score_1_15_xuhui_scores_2026_import_report.json|3/22|472|254|49|109|91||
18
+|徐汇区|median_score|上海戏剧学院附属高级中学|mps_score_1_15_xuhui_scores_2026_import_report.json|2/9|615.5|355.5|117|108|118||
19
+|徐汇区|max_score|上海交通大学附属中学|mps_score_1_15_xuhui_scores_2026_import_report.json|2/1|716|420|146|127|148||
20
+|徐汇区|zero_or_no_admission|上海市久隆模范中学|mps_score_1_15_xuhui_scores_2026_import_report.json|2/6|0|0|0|0|0|线上无人投档|
21
+|徐汇区|zero_or_no_admission|上海嘉定区民办华盛怀少学校|mps_score_1_15_xuhui_scores_2026_import_report.json|2/25|0|0|0|0|0|线上无人投档|
22
+|徐汇区|zero_or_no_admission|上海市松江区励滕高级中学|mps_score_1_15_xuhui_scores_2026_import_report.json|3/9|0|0|0|0|0|线上无人投档|
23
+|徐汇区|zero_or_no_admission|上海青浦区宏润博源高级中学|mps_score_1_15_xuhui_scores_2026_import_report.json|3/15|0|0|0|0|0|线上无人投档|
24
+|徐汇区|zero_or_no_admission|上海美达菲双语高级中学|mps_score_1_15_xuhui_scores_2026_import_report.json|3/18|0|0|0|0|0|线上无人投档|
25
+|徐汇区|half_point_digit|华东师范大学第二附属中学|mps_score_1_15_xuhui_scores_2026_import_report.json|2/3|715.5|423.5|146|131|146||
26
+|徐汇区|half_point_digit|上海市民办金苹果学校|mps_score_1_15_xuhui_scores_2026_import_report.json|3/3|545|319.5|88|102|107||
27
+|徐汇区|half_point_digit|上海新纪元双语学校|mps_score_1_15_xuhui_scores_2026_import_report.json|3/23|517.5|304.5|71|112|82|未录满|
28
+|徐汇区|half_point_digit|上海市零陵中学|mps_score_1_15_xuhui_scores_2026_import_report.json|1/14|638|371.5|117|119|126||
29
+|徐汇区|half_point_digit|上海民办行中中学|mps_score_1_15_xuhui_scores_2026_import_report.json|2/16|508.5|272|80|83|110||
30
+|宝山区|min_score|上海新纪元双语学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|2/29|462||||||
31
+|宝山区|median_score|上海市淞浦中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/9|575.5|329.5|113|126|105||
32
+|宝山区|max_score|上海市上海中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/24|717.5|426|150|131|145||
33
+|宝山区|total_only|上海民办行中中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/14|480||||||
34
+|宝山区|total_only|上海宝山区民办维尚高级中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/16|501||||||
35
+|宝山区|total_only|上海创艺高级中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/17|485||||||
36
+|宝山区|total_only|上海市同洲模范学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/19|501||||||
37
+|宝山区|total_only|上海金瑞学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/20|501||||||
38
+|宝山区|total_only|上海安生学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/30|501||||||
39
+|宝山区|half_point_digit|上海市崇明区堡镇中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|2/27|599.5|347|100|118|115||
40
+|宝山区|half_point_digit|上海市行知实验中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/11|611|366.5|116|117|109||
41
+|宝山区|half_point_digit|民办上海工商外国语职业学院附属中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|2/10|562.5|335|95|118|97||
42
+|宝山区|half_point_digit|上海宝山区世外学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/22|670.5|394.5|126|125|135||
43
+|宝山区|half_point_digit|华东师范大学第二附属中学(宝山校区)|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/宝山区.pdf|1/6|704.5|412.5|137|128|146||
44
+|浦东新区|min_score|上海浦东新区民办东鼎外国语学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/60|480.5|262.5|57|105|92|未录满|
45
+|浦东新区|median_score|上海市香山中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|1/17|619.5|359|120|117|129||
46
+|浦东新区|max_score|上海市上海中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|1/1|715.5|425.5|149|133|144||
47
+|浦东新区|zero_or_no_admission|上海宝山区民办维尚高级中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/85|0|0|0|0|0|未录满|
48
+|浦东新区|zero_or_no_admission|上海华旭双语学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/87|0|0|0|0|0|未录满|
49
+|浦东新区|zero_or_no_admission|上海市民办永昌中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/90|0|0|0|0|0|未录满|
50
+|浦东新区|zero_or_no_admission|上海市松江区励滕高级中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/92|0|0|0|0|0|未录满|
51
+|浦东新区|half_point_digit|上海市泥城中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|1/50|534|285.5|107|118|112||
52
+|浦东新区|half_point_digit|华东师范大学第二附属中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|1/4|715|425.5|141|138|143||
53
+|浦东新区|half_point_digit|上海金山区世外学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/89|639|390.5|116|128|114||
54
+|浦东新区|half_point_digit|上海市民办交大南洋中学|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/88|560.5|308|94|104|123||
55
+|浦东新区|half_point_digit|上海安生学校|/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区1.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区2.jpg,/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/浦东新区3.jpg|2/73|608.5|365.5|111|115|109|未录满|
56
+|青浦区|min_score|上海民办民一中学|青浦区.html|/53|451.5|227.5|105|88|95||
57
+|青浦区|median_score|上海浦东新区民办东鼎外国语学校|青浦区.html|/26|579.5|339.5|107|109|104||
58
+|青浦区|max_score|上海市上海中学|青浦区.html|/1|718.5|426.5|147|137|144||
59
+|青浦区|zero_or_no_admission|上海安生学校|青浦区.html|/4|0|0|0|0|0|未录满|
60
+|青浦区|zero_or_no_admission|上海圣华紫竹高级中学|青浦区.html|/9|0|0|0|0|0|未录满|
61
+|青浦区|zero_or_no_admission|上海闵行区诺达双语学校|青浦区.html|/10|0|0|0|0|0|未录满|
62
+|青浦区|zero_or_no_admission|上海金瑞学校|青浦区.html|/16|0|0|0|0|0|未录满|
63
+|青浦区|zero_or_no_admission|上海华旭双语学校|青浦区.html|/19|0|0|0|0|0|未录满|
64
+|青浦区|zero_or_no_admission|上海嘉定区民办华盛怀少学校|青浦区.html|/20|0|0|0|0|0|未录满|
65
+|青浦区|half_point_digit|上海宋庆龄学校|青浦区.html|/47|614.5|358.5|101|112|118||
66
+|青浦区|half_point_digit|上海青浦区协和双语学校|青浦区.html|/48|530.5|304.5|95|102|94|未录满|
67
+|青浦区|half_point_digit|上海市民办尚德实验学校|青浦区.html|/27|586.5|343.5|96|119|113||
68
+|青浦区|half_point_digit|上海存志高级中学|青浦区.html|/12|641.5|373.5|113|121|128||
69
+|青浦区|half_point_digit|上海交通大学附属中学|青浦区.html|/6|716.5|422.5|148|127|145||
70
+|崇明区|min_score|上海民办民一中学|崇明区1.jpeg|/27|441.5|228|40|100|82|未录满|
71
+|崇明区|median_score|上海戏剧学院附属高级中学|崇明区1.jpeg|/3|596|354|98|126|105||
72
+|崇明区|max_score|复旦大学附属中学|崇明区1.jpeg|/7|711.5|422.5|139|140|143||
73
+|崇明区|zero_or_no_admission|上海安生学校|崇明区1.jpeg|/4|0|0|0|0|0|无考生录取|
74
+|崇明区|zero_or_no_admission|上海创艺高级中学|崇明区1.jpeg|/10|0|0|0|0|0|无考生录取|
75
+|崇明区|zero_or_no_admission|上海市民办丰华高级中学|崇明区1.jpeg|/13|0|0|0|0|0|无考生录取|
76
+|崇明区|zero_or_no_admission|上海市松江区励滕高级中学|崇明区1.jpeg|/15|0|0|0|0|0|无考生录取|
77
+
78
+## 人工视觉复核
79
+- 宝山区.pdf page 2: 确认上海民办民一中学为 483 / 311 / 70 / 110 / 59,综合测试 59 不是水印误读。
80
+- 崇明区1.jpeg: 确认上海民办民一中学为 441.5 / 228 / 40 / 100 / 82,上海新纪元双语学校为 464.5 / 236.5 / 67 / 98 / 94。
81
+- 浦东新区.pdf page 2-3: 确认低分段和未录满行,包括东鼎外国语 480.5 / 262.5 / 57 / 105 / 92,民一中学 483.5 / 242.5 / 74 / 117 / 105,新纪元 480.5 / 259.5 / 92 / 122 / 85。
82
+- 徐汇区.pdf page 3: 确认补录第 3 页 23 行,重点核对浦实 620.5、东鼎 576.5、励滕线上无人投档、民一 472、新纪元 517.5。
83
+- 青浦区.html: 确认当前 HTML 54 行全匹配数据库,宏润博源为 493.5,不是 0 分;正式核查 changed_rows=0。
84
+
85
+## 已知源/库不一致
86
+- 长宁区: 源57,匹配57,库58,问题0,库未出现在源1
87
+- 静安区: 源65,匹配62,库63,问题3,库未出现在源1
88
+- 杨浦区: 源56,匹配55,库55,问题1,库未出现在源0
89
+- 浦东新区: 源106,匹配106,库107,问题0,库未出现在源1
90
+- 金山区: 源40,匹配40,库42,问题0,库未出现在源2
91
+- 松江区: 源58,匹配56,库61,问题2,库未出现在源5
92
+
93
+详细清单见 `mps_score_1_15_2026_sample_audit_report.json`。

Разлика између датотеке није приказан због своје велике величине
+ 7988 - 0
秒过分数线数据导入/mps_score_1_15_all_extracted_sources_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 1145 - 0
秒过分数线数据导入/mps_score_1_15_baoshan_scores_2026_import_report.json


Разлика између датотеке није приказан због своје велике величине
+ 5084 - 0
秒过分数线数据导入/mps_score_1_15_batch_extracted_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 15722 - 0
秒过分数线数据导入/mps_score_1_15_batch_import_report_2026.json


+ 68 - 0
秒过分数线数据导入/mps_score_1_15_changning_image_extracted_2026.json

@@ -0,0 +1,68 @@
1
+{
2
+  "districts": [
3
+    {
4
+      "districtId": 3,
5
+      "district": "长宁区",
6
+      "source": "长宁区1.png",
7
+      "rows": [
8
+        { "source": "长宁区1.png", "row": 1, "officialCode": "052001", "schoolName": "上海市第三女子中学", "scoreTotal": 685.5 },
9
+        { "source": "长宁区1.png", "row": 2, "officialCode": "052002", "schoolName": "上海市延安中学", "scoreTotal": 694, "score1": 415, "score2": 144, "score3": 124, "score4": 139 },
10
+        { "source": "长宁区1.png", "row": 3, "officialCode": "053003", "schoolName": "华东师范大学附属天山学校", "scoreTotal": 662.5 },
11
+        { "source": "长宁区1.png", "row": 4, "officialCode": "053004", "schoolName": "上海市复旦中学", "scoreTotal": 690, "score1": 404, "score2": 133, "score3": 129, "score4": 142 },
12
+        { "source": "长宁区1.png", "row": 5, "officialCode": "053005", "schoolName": "上海市建青实验学校", "scoreTotal": 653, "score1": 387, "score2": 117, "score3": 126, "score4": 131 },
13
+        { "source": "长宁区1.png", "row": 6, "officialCode": "054010", "schoolName": "上海市西郊学校", "scoreTotal": 616, "score1": 363, "score2": 111, "score3": 125, "score4": 116 },
14
+        { "source": "长宁区1.png", "row": 7, "officialCode": "054013", "schoolName": "华东政法大学附属中学", "scoreTotal": 645, "score1": 385, "score2": 124, "score3": 117, "score4": 119 },
15
+        { "source": "长宁区1.png", "row": 8, "officialCode": "054030", "schoolName": "上海市仙霞高级中学", "scoreTotal": 635.5, "score1": 362.5, "score2": 98, "score3": 126, "score4": 129 },
16
+        { "source": "长宁区1.png", "row": 9, "officialCode": "054043", "schoolName": "上海市民办新虹桥中学", "scoreTotal": 503, "officialRemark": "未招满" },
17
+        { "source": "长宁区1.png", "row": 10, "officialCode": "014006", "schoolName": "上海市同济黄浦设计创意中学", "scoreTotal": 649 },
18
+        { "source": "长宁区1.png", "row": 11, "officialCode": "042032", "schoolName": "上海市上海中学", "scoreTotal": 713.5 },
19
+        { "source": "长宁区1.png", "row": 12, "officialCode": "044083", "schoolName": "上海市民办西南高级中学", "scoreTotal": 630 },
20
+        { "source": "长宁区1.png", "row": 13, "officialCode": "063008", "schoolName": "上海市久隆模范中学", "scoreTotal": 597 },
21
+        { "source": "长宁区1.png", "row": 14, "officialCode": "069004", "schoolName": "上海戏剧学院附属高级中学(艺术班)", "scoreTotal": 616 },
22
+        { "source": "长宁区1.png", "row": 15, "officialCode": "074081", "schoolName": "上海安生学校", "scoreTotal": 591 },
23
+        { "source": "长宁区1.png", "row": 16, "officialCode": "079016", "schoolName": "上海音乐学院附属安师实验中学(艺术班)", "scoreTotal": 598 },
24
+        { "source": "长宁区1.png", "row": 17, "officialCode": "102056", "schoolName": "上海交通大学附属中学", "scoreTotal": 714.5 },
25
+        { "source": "长宁区1.png", "row": 18, "officialCode": "102057", "schoolName": "复旦大学附属中学", "scoreTotal": 715 },
26
+        { "source": "长宁区1.png", "row": 19, "officialCode": "124116", "schoolName": "上海市民办燎原双语高级中学", "scoreTotal": 607.5 },
27
+        { "source": "长宁区1.png", "row": 20, "officialCode": "125117", "schoolName": "上海闵行区诺达双语学校", "scoreTotal": 599.5, "officialRemark": "未招满" },
28
+        { "source": "长宁区1.png", "row": 21, "officialCode": "125119", "schoolName": "上海闵行区民办德闳学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
29
+        { "source": "长宁区1.png", "row": 22, "officialCode": "134010", "schoolName": "上海民办行中中学", "scoreTotal": 531.5 },
30
+        { "source": "长宁区1.png", "row": 23, "officialCode": "134012", "schoolName": "上海存志高级中学", "scoreTotal": 650 },
31
+        { "source": "长宁区1.png", "row": 24, "officialCode": "134013", "schoolName": "上海宝山区民办维尚高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
32
+        { "source": "长宁区1.png", "row": 25, "officialCode": "134014", "schoolName": "上海创艺高级中学", "scoreTotal": 546.5 },
33
+        { "source": "长宁区1.png", "row": 26, "officialCode": "134015", "schoolName": "上海市宝山华曜高级中学", "scoreTotal": 609.5 },
34
+        { "source": "长宁区1.png", "row": 27, "officialCode": "135001", "schoolName": "上海市同洲模范学校", "scoreTotal": 607.5 },
35
+        { "source": "长宁区1.png", "row": 28, "officialCode": "135003", "schoolName": "上海金瑞学校", "scoreTotal": 590.5 },
36
+        { "source": "长宁区1.png", "row": 29, "officialCode": "135034", "schoolName": "上海宝山区世外学校", "scoreTotal": 687.5 },
37
+        { "source": "长宁区1.png", "row": 30, "officialCode": "145007", "schoolName": "上海市民办远东学校", "scoreTotal": 602.5, "score1": 340, "score2": 136, "score3": 109, "score4": 124 },
38
+        { "source": "长宁区1.png", "row": 31, "officialCode": "145018", "schoolName": "上海华旭双语学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
39
+        { "source": "长宁区1.png", "row": 32, "officialCode": "145030", "schoolName": "上海嘉定区民办华盛怀少学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
40
+        { "source": "长宁区1.png", "row": 33, "officialCode": "152003", "schoolName": "华东师范大学第二附属中学", "scoreTotal": 712.5 },
41
+        { "source": "长宁区1.png", "row": 34, "officialCode": "152006", "schoolName": "上海师范大学附属中学", "scoreTotal": 702.5 },
42
+        { "source": "长宁区1.png", "row": 35, "officialCode": "154039", "schoolName": "上海市民办丰华高级中学", "scoreTotal": 610 },
43
+        { "source": "长宁区1.png", "row": 36, "officialCode": "154048", "schoolName": "民办上海工商外国语职业学院附属中学", "scoreTotal": 597 },
44
+        { "source": "长宁区1.png", "row": 37, "officialCode": "155013", "schoolName": "上海市民办金苹果学校", "scoreTotal": 602 },
45
+        { "source": "长宁区1.png", "row": 38, "officialCode": "155041", "schoolName": "上海浦东新区民办东鼎外国语学校", "scoreTotal": 598 },
46
+        { "source": "长宁区1.png", "row": 39, "officialCode": "155043", "schoolName": "上海市民办尚德实验学校", "scoreTotal": 644.5 },
47
+        { "source": "长宁区1.png", "row": 40, "officialCode": "164005", "schoolName": "上海市民办交大南洋中学", "scoreTotal": 591, "score1": 355, "score2": 109, "score3": 110, "score4": 102 },
48
+        { "source": "长宁区1.png", "row": 41, "officialCode": "164008", "schoolName": "上海市民办永昌中学", "scoreTotal": 575, "officialRemark": "未招满" },
49
+        { "source": "长宁区1.png", "row": 42, "officialCode": "165004", "schoolName": "上海金山区世外学校", "scoreTotal": 652.5 },
50
+        { "source": "长宁区1.png", "row": 43, "officialCode": "173002", "schoolName": "上海市松江区科德高级中学", "scoreTotal": 601.5, "officialRemark": "未招满" },
51
+        { "source": "长宁区1.png", "row": 44, "officialCode": "173005", "schoolName": "上海市松江区励滕高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
52
+        { "source": "长宁区1.png", "row": 45, "officialCode": "174013", "schoolName": "上海领科双语学校", "scoreTotal": 617 },
53
+        { "source": "长宁区1.png", "row": 46, "officialCode": "175018", "schoolName": "上海市西外外国语学校", "scoreTotal": 540.5 },
54
+        { "source": "长宁区1.png", "row": 47, "officialCode": "175024", "schoolName": "上海赫贤学校", "scoreTotal": 543 },
55
+        { "source": "长宁区1.png", "row": 48, "officialCode": "175025", "schoolName": "上海松江区爱菊学校", "scoreTotal": 635, "officialRemark": "未招满" },
56
+        { "source": "长宁区1.png", "row": 49, "officialCode": "184006", "schoolName": "上海青浦区世外高级中学", "scoreTotal": 611.5 },
57
+        { "source": "长宁区1.png", "row": 50, "officialCode": "184007", "schoolName": "上海青浦区宏润博源高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
58
+        { "source": "长宁区1.png", "row": 51, "officialCode": "185008", "schoolName": "上海宋庆龄学校", "scoreTotal": 670 },
59
+        { "source": "长宁区1.png", "row": 52, "officialCode": "185014", "schoolName": "上海青浦区协和双语学校", "scoreTotal": 535 },
60
+        { "source": "长宁区1.png", "row": 53, "officialCode": "204020", "schoolName": "上海美达菲双语高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "控分线上无考生录取" },
61
+        { "source": "长宁区1.png", "row": 54, "officialCode": "204021", "schoolName": "上海奉贤区博华高级中学", "scoreTotal": 601.5 },
62
+        { "source": "长宁区1.png", "row": 55, "officialCode": "514001", "schoolName": "上海市崇明区城桥中学", "scoreTotal": 547 },
63
+        { "source": "长宁区1.png", "row": 56, "officialCode": "514004", "schoolName": "上海市崇明区堡镇中学", "scoreTotal": 612.5 },
64
+        { "source": "长宁区1.png", "row": 57, "officialCode": "514010", "schoolName": "上海民办民一中学", "scoreTotal": 477 }
65
+      ]
66
+    }
67
+  ]
68
+}

+ 61 - 0
秒过分数线数据导入/mps_score_1_15_hongkou_image_extracted_2026.json

@@ -0,0 +1,61 @@
1
+{
2
+  "districts": [
3
+    {
4
+      "districtId": 6,
5
+      "district": "虹口区",
6
+      "source": "虹口区1.png",
7
+      "rows": [
8
+        { "source": "虹口区1.png", "row": 1, "schoolName": "上海市同济黄浦设计创意中学", "scoreTotal": 628, "score1": 360.5, "score2": 92, "score3": 128, "score4": 125 },
9
+        { "source": "虹口区1.png", "row": 2, "schoolName": "上海市上海中学", "scoreTotal": 716.5, "score1": 424.5, "score2": 143, "score3": 133, "score4": 146 },
10
+        { "source": "虹口区1.png", "row": 3, "schoolName": "上海田家炳中学", "scoreTotal": 653, "score1": 381, "score2": 127, "score3": 125, "score4": 133 },
11
+        { "source": "虹口区1.png", "row": 4, "schoolName": "上海市民办扬波中学", "scoreTotal": 584, "score1": 345, "score2": 108, "score3": 120, "score4": 106 },
12
+        { "source": "虹口区1.png", "row": 5, "schoolName": "上海市民办风范中学", "scoreTotal": 580, "score1": 331.5, "score2": 100, "score3": 108, "score4": 106 },
13
+        { "source": "虹口区1.png", "row": 6, "schoolName": "上海戏剧学院附属高级中学(艺术班)", "scoreTotal": 556.5, "score1": 312, "score2": 84, "score3": 121, "score4": 108 },
14
+        { "source": "虹口区1.png", "row": 7, "schoolName": "上海安生学校", "scoreTotal": 558, "score1": 306, "score2": 78, "score3": 116, "score4": 116 },
15
+        { "source": "虹口区1.png", "row": 8, "schoolName": "上海音乐学院附属安师实验中学(艺术班)", "scoreTotal": 687.5, "score1": 406.5, "score2": 142, "score3": 127, "score4": 136 },
16
+        { "source": "虹口区1.png", "row": 9, "schoolName": "复旦大学附属复兴中学", "scoreTotal": 693.5, "score1": 412.5, "score2": 133, "score3": 131, "score4": 140 },
17
+        { "source": "虹口区1.png", "row": 10, "schoolName": "华东师范大学第一附属中学", "scoreTotal": 682.5, "score1": 399.5, "score2": 126, "score3": 127, "score4": 137 },
18
+        { "source": "虹口区1.png", "row": 11, "schoolName": "上海财经大学附属北郊高级中学", "scoreTotal": 677.5, "score1": 392.5, "score2": 133, "score3": 114, "score4": 139 },
19
+        { "source": "虹口区1.png", "row": 12, "schoolName": "上海音乐学院虹口区北虹高级中学", "scoreTotal": 640.5, "score1": 369.5, "score2": 116, "score3": 125, "score4": 130 },
20
+        { "source": "虹口区1.png", "row": 13, "schoolName": "上海市继光高级中学", "scoreTotal": 588, "score1": 344.5, "score2": 87, "score3": 122, "score4": 110 },
21
+        { "source": "虹口区1.png", "row": 14, "schoolName": "上海市鲁迅中学", "scoreTotal": 628.5, "score1": 366.5, "score2": 119, "score3": 121, "score4": 124 },
22
+        { "source": "虹口区1.png", "row": 15, "schoolName": "上海市第五十二中学", "scoreTotal": 534, "score1": 294, "score2": 109, "score3": 109, "score4": 102 },
23
+        { "source": "虹口区1.png", "row": 16, "schoolName": "上海师范大学附属虹口中学", "scoreTotal": 654, "score1": 387, "score2": 116, "score3": 126, "score4": 128 },
24
+        { "source": "虹口区1.png", "row": 17, "schoolName": "同济大学附属澄衷中学", "scoreTotal": 612, "score1": 355, "score2": 98, "score3": 119, "score4": 115 },
25
+        { "source": "虹口区1.png", "row": 18, "schoolName": "上海交通大学附属中学", "scoreTotal": 713.5, "score1": 422.5, "score2": 150, "score3": 124, "score4": 143 },
26
+        { "source": "虹口区1.png", "row": 19, "schoolName": "复旦大学附属中学", "scoreTotal": 714.5, "score1": 421.5, "score2": 143, "score3": 131, "score4": 146 },
27
+        { "source": "虹口区1.png", "row": 20, "schoolName": "上海民办行中中学", "scoreTotal": 537, "score1": 313, "score2": 85, "score3": 114, "score4": 88 },
28
+        { "source": "虹口区1.png", "row": 21, "schoolName": "上海存志高级中学", "scoreTotal": 668.5, "score1": 400.5, "score2": 133, "score3": 128, "score4": 126 },
29
+        { "source": "虹口区1.png", "row": 22, "schoolName": "上海创艺高级中学", "scoreTotal": 581, "score1": 328.5, "score2": 101, "score3": 105, "score4": 119 },
30
+        { "source": "虹口区1.png", "row": 23, "schoolName": "上海市宝山华曜高级中学", "scoreTotal": 639, "score1": 363.5, "score2": 106, "score3": 118, "score4": 138 },
31
+        { "source": "虹口区1.png", "row": 24, "schoolName": "上海市同洲模范学校", "scoreTotal": 588, "score1": 343.5, "score2": 118, "score3": 112, "score4": 113 },
32
+        { "source": "虹口区1.png", "row": 25, "schoolName": "上海宝山区世外学校", "scoreTotal": 681.5, "score1": 394.5, "score2": 136, "score3": 117, "score4": 142 },
33
+        { "source": "虹口区1.png", "row": 26, "schoolName": "上海市民办远东学校", "scoreTotal": 560.5, "score1": 319, "score2": 101, "score3": 106, "score4": 102 },
34
+        { "source": "虹口区1.png", "row": 27, "schoolName": "华东师范大学第二附属中学", "scoreTotal": 712.5, "score1": 426.5, "score2": 150, "score3": 130, "score4": 139, "officialRemark": "图中简称:师大二附" },
35
+        { "source": "虹口区1.png", "row": 28, "schoolName": "上海市民办丰华高级中学", "scoreTotal": 581.5, "score1": 346.5, "score2": 99, "score3": 116, "score4": 100 },
36
+        { "source": "虹口区1.png", "row": 29, "schoolName": "上海市民办金苹果学校", "scoreTotal": 576.5, "score1": 331.5, "score2": 88, "score3": 105, "score4": 114 },
37
+        { "source": "虹口区1.png", "row": 30, "schoolName": "上海浦东新区民办东鼎外国语学校", "scoreTotal": 606.5, "score1": 333, "score2": 120, "score3": 99, "score4": 131 },
38
+        { "source": "虹口区1.png", "row": 31, "schoolName": "上海市民办尚德实验学校", "scoreTotal": 641.5, "score1": 369.5, "score2": 120, "score3": 113, "score4": 133 },
39
+        { "source": "虹口区1.png", "row": 32, "schoolName": "上海市民办交大南洋中学", "scoreTotal": 568, "score1": 320.5, "score2": 92, "score3": 106, "score4": 118 },
40
+        { "source": "虹口区1.png", "row": 33, "schoolName": "上海市民办永昌中学", "scoreTotal": 547.5, "score1": 280.5, "score2": 105, "score3": 106, "score4": 131 },
41
+        { "source": "虹口区1.png", "row": 34, "schoolName": "上海金山区世外学校", "scoreTotal": 662, "score1": 383, "score2": 112, "score3": 129, "score4": 136 },
42
+        { "source": "虹口区1.png", "row": 35, "schoolName": "上海领科双语学校", "scoreTotal": 678.5, "score1": 389, "score2": 130, "score3": 121, "score4": 143 },
43
+        { "source": "虹口区1.png", "row": 36, "schoolName": "上海市西外外国语学校", "scoreTotal": 563, "score1": 345.5, "score2": 104, "score3": 118, "score4": 88 },
44
+        { "source": "虹口区1.png", "row": 37, "schoolName": "上海赫贤学校", "scoreTotal": 580, "score1": 328, "score2": 87, "score3": 113, "score4": 108 },
45
+        { "source": "虹口区1.png", "row": 38, "schoolName": "上海青浦区世外高级中学", "scoreTotal": 628.5, "score1": 370.5, "score2": 128, "score3": 116, "score4": 120 },
46
+        { "source": "虹口区1.png", "row": 39, "schoolName": "上海宋庆龄学校", "scoreTotal": 526.5, "score1": 301.5, "score2": 61, "score3": 118, "score4": 95 },
47
+        { "source": "虹口区1.png", "row": 40, "schoolName": "上海青浦区协和双语学校", "scoreTotal": 588, "score1": 336.5, "score2": 91, "score3": 106, "score4": 116 },
48
+        { "source": "虹口区1.png", "row": 41, "schoolName": "上海美达菲双语高级中学", "scoreTotal": 533.5, "score1": 308.5, "score2": 93, "score3": 111, "score4": 92 },
49
+        { "source": "虹口区1.png", "row": 42, "schoolName": "上海奉贤区博华高级中学", "scoreTotal": 560, "score1": 307.5, "score2": 124, "score3": 114, "score4": 115 },
50
+        { "source": "虹口区1.png", "row": 43, "schoolName": "上海市崇明区城桥中学", "scoreTotal": 571.5, "score1": 330.5, "score2": 97, "score3": 123, "score4": 112, "officialRemark": "未招满" },
51
+        { "source": "虹口区1.png", "row": 44, "schoolName": "上海民办民一中学", "scoreTotal": 512, "score1": 289.5, "score2": 71, "score3": 90, "score4": 91 },
52
+        { "source": "虹口区1.png", "row": 45, "schoolName": "上海市久隆模范中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "/" },
53
+        { "source": "虹口区1.png", "row": 46, "schoolName": "上海宝山区民办维尚高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "/" },
54
+        { "source": "虹口区1.png", "row": 47, "schoolName": "上海金瑞学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "/" },
55
+        { "source": "虹口区1.png", "row": 48, "schoolName": "上海华旭双语学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "/" },
56
+        { "source": "虹口区1.png", "row": 49, "schoolName": "上海嘉定区民办华盛怀少学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "/" },
57
+        { "source": "虹口区1.png", "row": 50, "schoolName": "上海青浦区宏润博源高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "/" }
58
+      ]
59
+    }
60
+  ]
61
+}

+ 78 - 0
秒过分数线数据导入/mps_score_1_15_huangpu_image_extracted_2026.json

@@ -0,0 +1,78 @@
1
+{
2
+  "districts": [
3
+    {
4
+      "districtId": 1,
5
+      "district": "黄浦区",
6
+      "source": "黄浦区1.png",
7
+      "rows": [
8
+        { "source": "黄浦区1.png", "row": 1, "officialCode": "012001", "schoolName": "上海市格致中学", "scoreTotal": 691, "score1": 408.5, "score2": 137, "score3": 127 },
9
+        { "source": "黄浦区1.png", "row": 2, "officialCode": "012002", "schoolName": "上海市格致中学(奉贤校区)", "scoreTotal": 688.5, "score1": 400.5, "score2": 131, "score3": 126 },
10
+        { "source": "黄浦区1.png", "row": 3, "officialCode": "012003", "schoolName": "上海市大同中学", "scoreTotal": 695, "score1": 408, "score2": 141, "score3": 124 },
11
+        { "source": "黄浦区1.png", "row": 4, "officialCode": "012005", "schoolName": "上海市向明中学", "scoreTotal": 684.5, "score1": 407, "score2": 136, "score3": 125 },
12
+        { "source": "黄浦区1.png", "row": 5, "officialCode": "012006", "schoolName": "上海市向明中学(浦江校区)", "scoreTotal": 684.5, "score1": 398.5, "score2": 130, "score3": 122 },
13
+        { "source": "黄浦区1.png", "row": 6, "officialCode": "012007", "schoolName": "上海外国语大学附属大境中学", "scoreTotal": 674.5, "score1": 393.5, "score2": 141, "score3": 120 },
14
+        { "source": "黄浦区1.png", "row": 7, "officialCode": "012008", "schoolName": "上海市光明中学", "scoreTotal": 663, "score1": 393.5, "score2": 121, "score3": 130 },
15
+        { "source": "黄浦区1.png", "row": 8, "officialCode": "012009", "schoolName": "上海市敬业中学", "scoreTotal": 678.5, "score1": 397, "score2": 134, "score3": 119 },
16
+        { "source": "黄浦区1.png", "row": 9, "officialCode": "012010", "schoolName": "上海市卢湾高级中学", "scoreTotal": 669, "score1": 391, "score2": 126, "score3": 122 },
17
+        { "source": "黄浦区1.png", "row": 10, "officialCode": "012011", "schoolName": "同济大学科技中学", "scoreTotal": 709.5, "score1": 422.5, "score2": 142, "score3": 137 },
18
+        { "source": "黄浦区1.png", "row": 11, "officialCode": "013001", "schoolName": "上海市第八中学", "scoreTotal": 648.5, "score1": 368.5, "score2": 111, "score3": 116 },
19
+        { "source": "黄浦区1.png", "row": 12, "officialCode": "013003", "schoolName": "上海市第十中学", "scoreTotal": 637.5, "score1": 367.5, "score2": 116, "score3": 116 },
20
+        { "source": "黄浦区1.png", "row": 13, "officialCode": "013004", "schoolName": "上海理工大学附属储能中学", "scoreTotal": 628.5, "score1": 371.5, "score2": 123, "score3": 117 },
21
+        { "source": "黄浦区1.png", "row": 14, "officialCode": "014001", "schoolName": "上海市金陵中学", "scoreTotal": 593, "score1": 341.5, "score2": 101, "score3": 107 },
22
+        { "source": "黄浦区1.png", "row": 15, "officialCode": "014003", "schoolName": "上海市市南中学", "scoreTotal": 619.5, "score1": 356.5, "score2": 104, "score3": 119 },
23
+        { "source": "黄浦区1.png", "row": 16, "officialCode": "014004", "schoolName": "上海音乐学院附属黄浦比乐中学", "scoreTotal": 606, "score1": 340, "score2": 103, "score3": 121 },
24
+        { "source": "黄浦区1.png", "row": 17, "officialCode": "014006", "schoolName": "上海市同济黄浦设计创意中学", "scoreTotal": 617, "score1": 353, "score2": 110, "score3": 116 },
25
+        { "source": "黄浦区1.png", "row": 18, "officialCode": "042032", "schoolName": "上海市上海中学", "scoreTotal": 714.5, "score1": 426.5, "score2": 150, "score3": 132 },
26
+        { "source": "黄浦区1.png", "row": 19, "officialCode": "044083", "schoolName": "上海市民办西南高级中学", "scoreTotal": 597.5, "score1": 330.5, "score2": 116, "score3": 107 },
27
+        { "source": "黄浦区1.png", "row": 20, "officialCode": "063008", "schoolName": "上海市久隆模范中学", "scoreTotal": 652, "score1": 383, "score2": 113, "score3": 126 },
28
+        { "source": "黄浦区1.png", "row": 21, "officialCode": "064020", "schoolName": "上海田家炳中学", "scoreTotal": 660, "score1": 385.5, "score2": 111, "score3": 136 },
29
+        { "source": "黄浦区1.png", "row": 22, "officialCode": "064021", "schoolName": "上海市民办扬波中学", "scoreTotal": 598, "score1": 328, "score2": 104, "score3": 119 },
30
+        { "source": "黄浦区1.png", "row": 23, "officialCode": "064024", "schoolName": "上海市民办风范中学", "scoreTotal": 591, "score1": 345.5, "score2": 86, "score3": 123 },
31
+        { "source": "黄浦区1.png", "row": 24, "officialCode": "069004", "schoolName": "上海戏剧学院附属高级中学(艺术班)", "scoreTotal": 632, "score1": 360, "score2": 96, "score3": 130 },
32
+        { "source": "黄浦区1.png", "row": 25, "officialCode": "074081", "schoolName": "上海安生学校", "scoreTotal": 561.5, "score1": 312.5, "score2": 72, "score3": 109 },
33
+        { "source": "黄浦区1.png", "row": 26, "officialCode": "079016", "schoolName": "上海音乐学院附属安师实验中学(艺术班)", "scoreTotal": 609.5, "score1": 346.5, "score2": 107, "score3": 108, "officialRemark": "未录满" },
34
+        { "source": "黄浦区1.png", "row": 27, "officialCode": "102056", "schoolName": "上海交通大学附属中学", "scoreTotal": 713, "score1": 421.5, "score2": 143, "score3": 134 },
35
+        { "source": "黄浦区1.png", "row": 28, "officialCode": "102057", "schoolName": "复旦大学附属中学", "scoreTotal": 713, "score1": 426, "score2": 146, "score3": 133 },
36
+        { "source": "黄浦区1.png", "row": 29, "officialCode": "124116", "schoolName": "上海市民办燎原双语高级中学", "scoreTotal": 591.5, "score1": 353, "score2": 108, "score3": 113 },
37
+        { "source": "黄浦区1.png", "row": 30, "officialCode": "125117", "schoolName": "上海闵行区诺达双语学校", "scoreTotal": 545.5, "score1": 333, "score2": 77, "score3": 116 },
38
+        { "source": "黄浦区1.png", "row": 31, "officialCode": "125119", "schoolName": "上海闵行区民办德闳学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "officialRemark": "无考生达线" },
39
+        { "source": "黄浦区1.png", "row": 32, "officialCode": "134010", "schoolName": "上海民办行中中学", "scoreTotal": 584.5, "score1": 359.5, "score2": 102, "score3": 126 },
40
+        { "source": "黄浦区1.png", "row": 33, "officialCode": "134012", "schoolName": "上海存志高级中学", "scoreTotal": 676.5, "score1": 391.5, "score2": 134, "score3": 118 },
41
+        { "source": "黄浦区1.png", "row": 34, "officialCode": "134013", "schoolName": "上海宝山区民办维尚高级中学", "scoreTotal": 513, "score1": 276, "score2": 74, "score3": 120 },
42
+        { "source": "黄浦区1.png", "row": 35, "officialCode": "134014", "schoolName": "上海创艺高级中学", "scoreTotal": 534.5, "score1": 292.5, "score2": 83, "score3": 106 },
43
+        { "source": "黄浦区1.png", "row": 36, "officialCode": "134015", "schoolName": "上海市宝山华曜高级中学", "scoreTotal": 601.5, "score1": 349, "score2": 101, "score3": 116 },
44
+        { "source": "黄浦区1.png", "row": 37, "officialCode": "135001", "schoolName": "上海市同洲模范学校", "scoreTotal": 635.5, "score1": 369.5, "score2": 125, "score3": 122 },
45
+        { "source": "黄浦区1.png", "row": 38, "officialCode": "135003", "schoolName": "上海金瑞学校", "scoreTotal": 570, "score1": 318, "score2": 97, "score3": 115 },
46
+        { "source": "黄浦区1.png", "row": 39, "officialCode": "135034", "schoolName": "上海宝山区世外学校", "scoreTotal": 684, "score1": 407, "score2": 134, "score3": 131 },
47
+        { "source": "黄浦区1.png", "row": 40, "officialCode": "145007", "schoolName": "上海市民办远东学校", "scoreTotal": 588.5, "score1": 338, "score2": 104, "score3": 123 },
48
+        { "source": "黄浦区1.png", "row": 41, "officialCode": "145018", "schoolName": "上海华旭双语学校", "scoreTotal": 574.5, "score1": 326, "score2": 101, "score3": 111 },
49
+        { "source": "黄浦区1.png", "row": 42, "officialCode": "145030", "schoolName": "上海嘉定区民办华盛怀少学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "officialRemark": "无考生达线" },
50
+        { "source": "黄浦区1.png", "row": 43, "officialCode": "152003", "schoolName": "华东师范大学第二附属中学", "scoreTotal": 712.5, "score1": 421.5, "score2": 143, "score3": 131 },
51
+        { "source": "黄浦区1.png", "row": 44, "officialCode": "154033", "schoolName": "上海市浦东新区民办浦实高级中学", "scoreTotal": 589, "score1": 346.5, "score2": 102, "score3": 120 },
52
+        { "source": "黄浦区1.png", "row": 45, "officialCode": "154039", "schoolName": "上海市民办丰华高级中学", "scoreTotal": 599.5, "score1": 342, "score2": 98, "score3": 116 },
53
+        { "source": "黄浦区1.png", "row": 46, "officialCode": "154048", "schoolName": "民办上海工商外国语职业学院附属中学", "scoreTotal": 598.5, "score1": 341.5, "score2": 100, "score3": 113 },
54
+        { "source": "黄浦区1.png", "row": 47, "officialCode": "155013", "schoolName": "上海市民办金苹果学校", "scoreTotal": 592.5, "score1": 336.5, "score2": 85, "score3": 118 },
55
+        { "source": "黄浦区1.png", "row": 48, "officialCode": "155041", "schoolName": "上海浦东新区民办东鼎外国语学校", "scoreTotal": 596, "score1": 335, "score2": 94, "score3": 111 },
56
+        { "source": "黄浦区1.png", "row": 49, "officialCode": "155043", "schoolName": "上海市民办尚德实验学校", "scoreTotal": 632.5, "score1": 370, "score2": 108, "score3": 121 },
57
+        { "source": "黄浦区1.png", "row": 50, "officialCode": "164005", "schoolName": "上海市民办交大南洋中学", "scoreTotal": 579, "score1": 340.5, "score2": 104, "score3": 108 },
58
+        { "source": "黄浦区1.png", "row": 51, "officialCode": "164008", "schoolName": "上海市民办永昌中学", "scoreTotal": 565.5, "score1": 337, "score2": 113, "score3": 105 },
59
+        { "source": "黄浦区1.png", "row": 52, "officialCode": "165004", "schoolName": "上海金山区世外学校", "scoreTotal": 647.5, "score1": 372.5, "score2": 121, "score3": 111 },
60
+        { "source": "黄浦区1.png", "row": 53, "officialCode": "173002", "schoolName": "上海市松江区科德高级中学", "scoreTotal": 618.5, "score1": 361, "score2": 118, "score3": 107 },
61
+        { "source": "黄浦区1.png", "row": 54, "officialCode": "173005", "schoolName": "上海市松江区励滕高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "officialRemark": "无考生达线" },
62
+        { "source": "黄浦区1.png", "row": 55, "officialCode": "174013", "schoolName": "上海领科双语学校", "scoreTotal": 550.5, "score1": 315, "score2": 84, "score3": 107 },
63
+        { "source": "黄浦区1.png", "row": 56, "officialCode": "175018", "schoolName": "上海市西外外国语学校", "scoreTotal": 580.5, "score1": 336.5, "score2": 105, "score3": 111 },
64
+        { "source": "黄浦区1.png", "row": 57, "officialCode": "175024", "schoolName": "上海赫贤学校", "scoreTotal": 508.5, "score1": 276.5, "score2": 83, "score3": 105, "officialRemark": "未录满" },
65
+        { "source": "黄浦区1.png", "row": 58, "officialCode": "175025", "schoolName": "上海松江区爱菊学校", "scoreTotal": 697.5, "score1": 406.5, "score2": 133, "score3": 130 },
66
+        { "source": "黄浦区1.png", "row": 59, "officialCode": "184006", "schoolName": "上海青浦区世外高级中学", "scoreTotal": 680.5, "score1": 400, "score2": 126, "score3": 129 },
67
+        { "source": "黄浦区1.png", "row": 60, "officialCode": "184007", "schoolName": "上海青浦区宏润博源高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "officialRemark": "无考生达线" },
68
+        { "source": "黄浦区1.png", "row": 61, "officialCode": "185008", "schoolName": "上海宋庆龄学校", "scoreTotal": 571.5, "score1": 325.5, "score2": 98, "score3": 123 },
69
+        { "source": "黄浦区1.png", "row": 62, "officialCode": "185014", "schoolName": "上海青浦区协和双语学校", "scoreTotal": 587, "score1": 324, "score2": 99, "score3": 112 },
70
+        { "source": "黄浦区1.png", "row": 63, "officialCode": "204020", "schoolName": "上海美达菲双语高级中学", "scoreTotal": 518, "score1": 282, "score2": 81, "score3": 105 },
71
+        { "source": "黄浦区1.png", "row": 64, "officialCode": "204021", "schoolName": "上海奉贤区博华高级中学", "scoreTotal": 577.5, "score1": 320.5, "score2": 92, "score3": 114 },
72
+        { "source": "黄浦区1.png", "row": 65, "officialCode": "514001", "schoolName": "上海市崇明区城桥中学", "scoreTotal": 543.5, "score1": 319.5, "score2": 88, "score3": 113, "officialRemark": "未录满" },
73
+        { "source": "黄浦区1.png", "row": 66, "officialCode": "514004", "schoolName": "上海市崇明区堡镇中学", "scoreTotal": 655.5, "score1": 383.5, "score2": 123, "score3": 119 },
74
+        { "source": "黄浦区1.png", "row": 67, "officialCode": "514010", "schoolName": "上海民办民一中学", "scoreTotal": 564, "score1": 316.5, "score2": 99, "score3": 113 }
75
+      ]
76
+    }
77
+  ]
78
+}

+ 40 - 0
秒过分数线数据导入/mps_score_1_15_image_extracted_2026.json

@@ -0,0 +1,40 @@
1
+{
2
+  "districts": [
3
+    {
4
+      "districtId": 16,
5
+      "district": "崇明区",
6
+      "source": "崇明区1.jpeg",
7
+      "rows": [
8
+        { "source": "崇明区1.jpeg", "row": 1, "officialCode": "042032", "schoolName": "上海市上海中学", "scoreTotal": 705.5, "score1": 416.5, "score2": 142, "score3": 130, "score4": 141 },
9
+        { "source": "崇明区1.jpeg", "row": 2, "officialCode": "064020", "schoolName": "上海田家炳中学", "scoreTotal": 648.5, "score1": 373.5, "score2": 135, "score3": 124, "score4": 135 },
10
+        { "source": "崇明区1.jpeg", "row": 3, "officialCode": "069004", "schoolName": "上海戏剧学院附属高级中学(艺术班)", "scoreTotal": 596, "score1": 354, "score2": 98, "score3": 126, "score4": 105 },
11
+        { "source": "崇明区1.jpeg", "row": 4, "officialCode": "074081", "schoolName": "上海安生学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
12
+        { "source": "崇明区1.jpeg", "row": 5, "officialCode": "079016", "schoolName": "上海音乐学院附属安师实验中学(艺术班)", "scoreTotal": 567, "score1": 332, "score2": 99, "score3": 119, "score4": 98 },
13
+        { "source": "崇明区1.jpeg", "row": 6, "officialCode": "102056", "schoolName": "上海交通大学附属中学", "scoreTotal": 705, "score1": 416, "score2": 143, "score3": 130, "score4": 145 },
14
+        { "source": "崇明区1.jpeg", "row": 7, "officialCode": "102057", "schoolName": "复旦大学附属中学", "scoreTotal": 711.5, "score1": 422.5, "score2": 139, "score3": 140, "score4": 143 },
15
+        { "source": "崇明区1.jpeg", "row": 8, "officialCode": "134010", "schoolName": "上海民办行中中学", "scoreTotal": 487.5, "score1": 281.5, "score2": 98, "score3": 105, "score4": 82, "officialRemark": "未录满" },
16
+        { "source": "崇明区1.jpeg", "row": 9, "officialCode": "134012", "schoolName": "上海存志高级中学", "scoreTotal": 654.5, "score1": 380.5, "score2": 109, "score3": 126, "score4": 135 },
17
+        { "source": "崇明区1.jpeg", "row": 10, "officialCode": "134014", "schoolName": "上海创艺高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
18
+        { "source": "崇明区1.jpeg", "row": 11, "officialCode": "134015", "schoolName": "上海市宝山华曜高级中学", "scoreTotal": 543.5, "score1": 322.5, "score2": 112, "score3": 89, "score4": 102, "officialRemark": "未录满" },
19
+        { "source": "崇明区1.jpeg", "row": 12, "officialCode": "152003", "schoolName": "华东师范大学第二附属中学", "scoreTotal": 704.5, "score1": 419.5, "score2": 143, "score3": 132, "score4": 140 },
20
+        { "source": "崇明区1.jpeg", "row": 13, "officialCode": "154039", "schoolName": "上海市民办丰华高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
21
+        { "source": "崇明区1.jpeg", "row": 14, "officialCode": "155043", "schoolName": "上海市民办尚德实验学校", "scoreTotal": 643, "score1": 365, "score2": 138, "score3": 119, "score4": 136 },
22
+        { "source": "崇明区1.jpeg", "row": 15, "officialCode": "173005", "schoolName": "上海市松江区励滕高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
23
+        { "source": "崇明区1.jpeg", "row": 16, "officialCode": "175018", "schoolName": "上海市西外外国语学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
24
+        { "source": "崇明区1.jpeg", "row": 17, "officialCode": "175024", "schoolName": "上海赫贤学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
25
+        { "source": "崇明区1.jpeg", "row": 18, "officialCode": "175025", "schoolName": "上海松江区爱菊学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
26
+        { "source": "崇明区1.jpeg", "row": 19, "officialCode": "185014", "schoolName": "上海青浦区协和双语学校", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
27
+        { "source": "崇明区1.jpeg", "row": 20, "officialCode": "204020", "schoolName": "上海美达菲双语高级中学", "scoreTotal": 0, "score1": 0, "score2": 0, "score3": 0, "score4": 0, "officialRemark": "无考生录取" },
28
+        { "source": "崇明区1.jpeg", "row": 21, "officialCode": "512000", "schoolName": "上海市崇明中学", "scoreTotal": 655.5, "score1": 395, "score2": 132, "score3": 126, "score4": 125 },
29
+        { "source": "崇明区1.jpeg", "row": 22, "officialCode": "512001", "schoolName": "上海市实验学校东滩高级中学", "scoreTotal": 661, "score1": 391.5, "score2": 131, "score3": 120, "score4": 127 },
30
+        { "source": "崇明区1.jpeg", "row": 23, "officialCode": "513001", "schoolName": "上海市崇明区民本中学", "scoreTotal": 594, "score1": 350.5, "score2": 100, "score3": 113, "score4": 108 },
31
+        { "source": "崇明区1.jpeg", "row": 24, "officialCode": "513009", "schoolName": "上海市扬子中学", "scoreTotal": 585, "score1": 323, "score2": 121, "score3": 115, "score4": 123 },
32
+        { "source": "崇明区1.jpeg", "row": 25, "officialCode": "514001", "schoolName": "上海市崇明区城桥中学", "scoreTotal": 524.5, "score1": 311.5, "score2": 88, "score3": 108, "score4": 89 },
33
+        { "source": "崇明区1.jpeg", "row": 26, "officialCode": "514004", "schoolName": "上海市崇明区堡镇中学", "scoreTotal": 522, "score1": 285, "score2": 98, "score3": 104, "score4": 102 },
34
+        { "source": "崇明区1.jpeg", "row": 27, "officialCode": "514010", "schoolName": "上海民办民一中学", "scoreTotal": 441.5, "score1": 228, "score2": 40, "score3": 100, "score4": 82, "officialRemark": "未录满" },
35
+        { "source": "崇明区1.jpeg", "row": 28, "officialCode": "514013", "schoolName": "上海市崇明区横沙中学", "scoreTotal": 505, "score1": 262, "score2": 102, "score3": 99, "score4": 116 },
36
+        { "source": "崇明区1.jpeg", "row": 29, "officialCode": "514014", "schoolName": "上海新纪元双语学校", "scoreTotal": 464.5, "score1": 236.5, "score2": 67, "score3": 98, "score4": 94, "officialRemark": "未录满" }
37
+      ]
38
+    }
39
+  ]
40
+}

+ 91 - 0
秒过分数线数据导入/mps_score_1_15_minhang_image_extracted_2026.json

@@ -0,0 +1,91 @@
1
+{
2
+  "districts": [
3
+    {
4
+      "districtId": 8,
5
+      "district": "闵行区",
6
+      "source": "闵行区1.jpg + 闵行区2.jpg",
7
+      "rows": [
8
+        { "source": "闵行区1.jpg", "row": 1, "officialCode": "012006", "schoolName": "上海市向明中学(浦江校区)", "scoreTotal": 696, "score1": 411, "score2": 143, "score3": 128, "score4": 143 },
9
+        { "source": "闵行区1.jpg", "row": 2, "officialCode": "014006", "schoolName": "上海市同济黄浦设计创意中学", "scoreTotal": 646, "score1": 377, "score2": 112, "score3": 127, "score4": 128 },
10
+        { "source": "闵行区1.jpg", "row": 3, "officialCode": "042002", "schoolName": "上海市第二中学(梅陇校区)", "scoreTotal": 698.5, "score1": 409.5, "score2": 143, "score3": 125, "score4": 142 },
11
+        { "source": "闵行区1.jpg", "row": 4, "officialCode": "042032", "schoolName": "上海市上海中学", "scoreTotal": 718, "score1": 424, "score2": 142, "score3": 134, "score4": 148 },
12
+        { "source": "闵行区1.jpg", "row": 5, "officialCode": "044083", "schoolName": "上海市民办西南高级中学", "scoreTotal": 633, "score1": 370, "score2": 112, "score3": 118, "score4": 121 },
13
+        { "source": "闵行区1.jpg", "row": 6, "officialCode": "063008", "schoolName": "上海市久隆模范中学", "scoreTotal": 615, "score1": 356, "score2": 106, "score3": 123, "score4": 117 },
14
+        { "source": "闵行区1.jpg", "row": 7, "officialCode": "064020", "schoolName": "上海田家炳中学", "scoreTotal": 650, "score1": 376.5, "score2": 126, "score3": 123, "score4": 136 },
15
+        { "source": "闵行区1.jpg", "row": 8, "officialCode": "069004", "schoolName": "上海戏剧学院附属高级中学(艺术班)", "scoreTotal": 617, "score1": 353, "score2": 109, "score3": 124, "score4": 120 },
16
+        { "source": "闵行区1.jpg", "row": 9, "officialCode": "074081", "schoolName": "上海安生学校", "scoreTotal": 502.5, "score1": 283.5, "score2": 80, "score3": 110, "score4": 97, "officialRemark": "未招满" },
17
+        { "source": "闵行区1.jpg", "row": 10, "officialCode": "079016", "schoolName": "上海音乐学院附属安师实验中学(艺术班)", "scoreTotal": 607.5, "score1": 357.5, "score2": 109, "score3": 116, "score4": 112 },
18
+        { "source": "闵行区1.jpg", "row": 11, "officialCode": "102056", "schoolName": "上海交通大学附属中学", "scoreTotal": 718, "score1": 420, "score2": 144, "score3": 129, "score4": 149 },
19
+        { "source": "闵行区1.jpg", "row": 12, "officialCode": "102057", "schoolName": "复旦大学附属中学", "scoreTotal": 717.5, "score1": 424.5, "score2": 150, "score3": 130, "score4": 146 },
20
+        { "source": "闵行区1.jpg", "row": 13, "officialCode": "122001", "schoolName": "上海市七宝中学", "scoreTotal": 705, "score1": 415, "score2": 144, "score3": 127, "score4": 146 },
21
+        { "source": "闵行区1.jpg", "row": 14, "officialCode": "122002", "schoolName": "华东师范大学第二附属中学闵行紫竹分校", "scoreTotal": 702.5, "score1": 408.5, "score2": 133, "score3": 129, "score4": 145 },
22
+        { "source": "闵行区1.jpg", "row": 15, "officialCode": "122003", "schoolName": "上海师范大学附属中学闵行分校", "scoreTotal": 699.5, "score1": 410, "score2": 142, "score3": 124, "score4": 144 },
23
+        { "source": "闵行区1.jpg", "row": 16, "officialCode": "122004", "schoolName": "上海交通大学附属中学闵行分校", "scoreTotal": 704.5, "score1": 419.5, "score2": 143, "score3": 129, "score4": 139 },
24
+        { "source": "闵行区1.jpg", "row": 17, "officialCode": "123001", "schoolName": "上海市闵行中学", "scoreTotal": 694, "score1": 401, "score2": 125, "score3": 133, "score4": 147 },
25
+        { "source": "闵行区1.jpg", "row": 18, "officialCode": "123002", "schoolName": "上海市莘庄中学", "scoreTotal": 679, "score1": 397, "score2": 129, "score3": 122, "score4": 135 },
26
+        { "source": "闵行区1.jpg", "row": 19, "officialCode": "123003", "schoolName": "上海外国语大学闵行外国语中学", "scoreTotal": 688.5, "score1": 403.5, "score2": 141, "score3": 124, "score4": 144 },
27
+        { "source": "闵行区1.jpg", "row": 20, "officialCode": "124005", "schoolName": "华东理工大学附属闵行科技高级中学", "scoreTotal": 661, "score1": 382.5, "score2": 131, "score3": 115, "score4": 139 },
28
+        { "source": "闵行区1.jpg", "row": 21, "officialCode": "124006", "schoolName": "上海市闵行第三中学", "scoreTotal": 666.5, "officialRemark": "同分优待:是" },
29
+        { "source": "闵行区1.jpg", "row": 22, "officialCode": "124011", "schoolName": "上海市七宝中学附属鑫都实验中学", "scoreTotal": 677, "score1": 398, "score2": 125, "score3": 128, "score4": 135 },
30
+        { "source": "闵行区1.jpg", "row": 23, "officialCode": "124012", "schoolName": "上海市闵行中学东校", "scoreTotal": 654, "score1": 377, "score2": 118, "score3": 122, "score4": 136 },
31
+        { "source": "闵行区1.jpg", "row": 24, "officialCode": "124013", "schoolName": "上海市闵行区实验高级中学", "scoreTotal": 639.5, "officialRemark": "同分优待:是" },
32
+        { "source": "闵行区1.jpg", "row": 25, "officialCode": "124015", "schoolName": "上海市七宝中学浦江分校", "scoreTotal": 673.5, "score1": 393.5, "score2": 129, "score3": 126, "score4": 138 },
33
+        { "source": "闵行区1.jpg", "row": 26, "officialCode": "124019", "schoolName": "上海中医药大学附属浦江高级中学", "scoreTotal": 620, "score1": 357.5, "score2": 94, "score3": 125, "score4": 125 },
34
+        { "source": "闵行区1.jpg", "row": 27, "officialCode": "124020", "schoolName": "上海市金汇高级中学", "scoreTotal": 614.5, "score1": 345.5, "score2": 113, "score3": 115, "score4": 127 },
35
+        { "source": "闵行区1.jpg", "row": 28, "officialCode": "124022", "schoolName": "北京外国语大学附属上海闵行田园高级中学", "scoreTotal": 652, "score1": 372, "score2": 115, "score3": 122, "score4": 135 },
36
+        { "source": "闵行区1.jpg", "row": 29, "officialCode": "124024", "schoolName": "上海市古美高级中学", "scoreTotal": 634, "score1": 374, "score2": 123, "score3": 119, "score4": 118 },
37
+        { "source": "闵行区1.jpg", "row": 30, "officialCode": "124108", "schoolName": "上海市文来中学", "scoreTotal": 665, "score1": 385, "score2": 116, "score3": 127, "score4": 132 },
38
+        { "source": "闵行区1.jpg", "row": 31, "officialCode": "124109", "schoolName": "上海市民办文绮中学", "scoreTotal": 623.5, "score1": 355, "score2": 113, "score3": 122, "score4": 131 },
39
+        { "source": "闵行区1.jpg", "row": 32, "officialCode": "124114", "schoolName": "上海市闵行区教育学院附属中学", "scoreTotal": 607, "score1": 371.5, "score2": 123, "score3": 118, "score4": 100 },
40
+        { "source": "闵行区1.jpg", "row": 33, "officialCode": "124115", "schoolName": "上海闵行区协和双语教科学校", "scoreTotal": 598.5, "score1": 359.5, "score2": 100, "score3": 126, "score4": 101 },
41
+        { "source": "闵行区1.jpg", "row": 34, "officialCode": "124116", "schoolName": "上海市民办燎原双语高级中学", "scoreTotal": 596.5, "score1": 337.5, "score2": 92, "score3": 109, "score4": 121 },
42
+        { "source": "闵行区1.jpg", "row": 35, "officialCode": "124117", "schoolName": "上海闵行区诺美高级中学", "scoreTotal": 508, "score1": 295, "score2": 77, "score3": 120, "score4": 79, "officialRemark": "未招满" },
43
+        { "source": "闵行区1.jpg", "row": 36, "officialCode": "124118", "schoolName": "上海圣华紫竹高级中学", "scoreTotal": 509, "score1": 293, "score2": 71, "score3": 116, "score4": 84, "officialRemark": "未招满" },
44
+        { "source": "闵行区1.jpg", "row": 37, "officialCode": "125008", "schoolName": "上海交通大学附属闵行实验学校", "scoreTotal": 680, "score1": 401, "score2": 124, "score3": 130, "score4": 134 },
45
+        { "source": "闵行区1.jpg", "row": 38, "officialCode": "125028", "schoolName": "华东师范大学附属闵行永德学校", "scoreTotal": 645.5, "score1": 374.5, "score2": 130, "score3": 124, "score4": 133 },
46
+        { "source": "闵行区1.jpg", "row": 39, "officialCode": "125115", "schoolName": "上海闵行区万科双语学校", "scoreTotal": 558, "score1": 323.5, "score2": 94, "score3": 121, "score4": 104 },
47
+        { "source": "闵行区1.jpg", "row": 40, "officialCode": "125117", "schoolName": "上海闵行区诺达双语学校", "scoreTotal": 509.5, "score1": 318, "score2": 87, "score3": 94, "score4": 84, "officialRemark": "未招满" },
48
+        { "source": "闵行区1.jpg", "row": 41, "officialCode": "125119", "schoolName": "上海闵行区民办德闳学校", "scoreTotal": 507.5, "score1": 304.5, "score2": 94, "score3": 100, "score4": 75, "officialRemark": "未招满" },
49
+        { "source": "闵行区1.jpg", "row": 42, "officialCode": "134010", "schoolName": "上海民办行中中学", "scoreTotal": 486.5, "score1": 282, "score2": 82, "score3": 105, "score4": 83 },
50
+        { "source": "闵行区1.jpg", "row": 43, "officialCode": "134012", "schoolName": "上海存志高级中学", "scoreTotal": 621, "score1": 355, "score2": 110, "score3": 115, "score4": 124 },
51
+        { "source": "闵行区1.jpg", "row": 44, "officialCode": "134013", "schoolName": "上海宝山区民办维尚高级中学", "scoreTotal": 501, "officialRemark": "未招满" },
52
+        { "source": "闵行区1.jpg", "row": 45, "officialCode": "134014", "schoolName": "上海创艺高级中学", "scoreTotal": 488, "score1": 293, "score2": 79, "score3": 100, "score4": 65, "officialRemark": "未招满" },
53
+        { "source": "闵行区2.jpg", "row": 46, "officialCode": "134015", "schoolName": "上海市宝山华曜高级中学", "scoreTotal": 576.5, "score1": 332, "score2": 97, "score3": 107, "score4": 105 },
54
+        { "source": "闵行区2.jpg", "row": 47, "officialCode": "135001", "schoolName": "上海市同洲模范学校", "scoreTotal": 569.5, "score1": 308, "score2": 96, "score3": 112, "score4": 123 },
55
+        { "source": "闵行区2.jpg", "row": 48, "officialCode": "135003", "schoolName": "上海金瑞学校", "scoreTotal": 506, "score1": 277, "score2": 115, "score3": 108, "score4": 96, "officialRemark": "未招满" },
56
+        { "source": "闵行区2.jpg", "row": 49, "officialCode": "135034", "schoolName": "上海宝山区世外学校", "scoreTotal": 685.5, "score1": 399.5, "score2": 127, "score3": 128, "score4": 142 },
57
+        { "source": "闵行区2.jpg", "row": 50, "officialCode": "145007", "schoolName": "上海市民办远东学校", "scoreTotal": 592.5, "score1": 366.5, "score2": 103, "score3": 121, "score4": 92 },
58
+        { "source": "闵行区2.jpg", "row": 51, "officialCode": "145018", "schoolName": "上海华旭双语学校", "scoreTotal": 549, "score1": 310.5, "score2": 105, "score3": 109, "score4": 111, "officialRemark": "未招满" },
59
+        { "source": "闵行区2.jpg", "row": 52, "officialCode": "145030", "schoolName": "上海嘉定区民办华盛怀少学校", "scoreTotal": 501, "officialRemark": "未招满" },
60
+        { "source": "闵行区2.jpg", "row": 53, "officialCode": "152003", "schoolName": "华东师范大学第二附属中学", "scoreTotal": 717.5, "score1": 425, "score2": 147, "score3": 133, "score4": 149 },
61
+        { "source": "闵行区2.jpg", "row": 54, "officialCode": "152006", "schoolName": "上海师范大学附属中学", "scoreTotal": 702, "score1": 416, "score2": 146, "score3": 122, "score4": 145 },
62
+        { "source": "闵行区2.jpg", "row": 55, "officialCode": "154033", "schoolName": "上海市浦东新区民办浦实高级中学", "scoreTotal": 560.5, "score1": 309.5, "score2": 101, "score3": 121, "score4": 112 },
63
+        { "source": "闵行区2.jpg", "row": 56, "officialCode": "154039", "schoolName": "上海市民办丰华高级中学", "scoreTotal": 594, "score1": 332, "score2": 107, "score3": 109, "score4": 123 },
64
+        { "source": "闵行区2.jpg", "row": 57, "officialCode": "154048", "schoolName": "民办上海工商外国语职业学院附属中学", "scoreTotal": 588, "score1": 342.5, "score2": 120, "score3": 110, "score4": 117 },
65
+        { "source": "闵行区2.jpg", "row": 58, "officialCode": "155013", "schoolName": "上海市民办金苹果学校", "scoreTotal": 561.5, "score1": 303, "score2": 96, "score3": 117, "score4": 122 },
66
+        { "source": "闵行区2.jpg", "row": 59, "officialCode": "155041", "schoolName": "上海浦东新区民办东鼎外国语学校", "scoreTotal": 570, "score1": 322, "score2": 77, "score3": 112, "score4": 110 },
67
+        { "source": "闵行区2.jpg", "row": 60, "officialCode": "155043", "schoolName": "上海市民办尚德实验学校", "scoreTotal": 633.5, "score1": 372.5, "score2": 126, "score3": 107, "score4": 131 },
68
+        { "source": "闵行区2.jpg", "row": 61, "officialCode": "164005", "schoolName": "上海市民办交大南洋中学", "scoreTotal": 579, "score1": 330, "score2": 87, "score3": 109, "score4": 106 },
69
+        { "source": "闵行区2.jpg", "row": 62, "officialCode": "164008", "schoolName": "上海市民办永昌中学", "scoreTotal": 501.5, "score1": 269.5, "score2": 74, "score3": 112, "score4": 92, "officialRemark": "未招满" },
70
+        { "source": "闵行区2.jpg", "row": 63, "officialCode": "165004", "schoolName": "上海金山区世外学校", "scoreTotal": 636.5, "score1": 363, "score2": 114, "score3": 113, "score4": 130 },
71
+        { "source": "闵行区2.jpg", "row": 64, "officialCode": "173002", "schoolName": "上海市松江区科德高级中学", "scoreTotal": 544.5, "score1": 322.5, "score2": 104, "score3": 90, "score4": 94, "officialRemark": "未招满" },
72
+        { "source": "闵行区2.jpg", "row": 65, "officialCode": "173005", "schoolName": "上海市松江区励滕高级中学", "scoreTotal": 501, "officialRemark": "未招满" },
73
+        { "source": "闵行区2.jpg", "row": 66, "officialCode": "174008", "schoolName": "上海市松江九峰实验学校", "scoreTotal": 653.5, "score1": 382.5, "score2": 116, "score3": 130, "score4": 128 },
74
+        { "source": "闵行区2.jpg", "row": 67, "officialCode": "174013", "schoolName": "上海领科双语学校", "scoreTotal": 603, "score1": 355, "score2": 99, "score3": 123, "score4": 113 },
75
+        { "source": "闵行区2.jpg", "row": 68, "officialCode": "175018", "schoolName": "上海市西外外国语学校", "scoreTotal": 569, "score1": 332, "score2": 84, "score3": 125, "score4": 99 },
76
+        { "source": "闵行区2.jpg", "row": 69, "officialCode": "175024", "schoolName": "上海赫贤学校", "scoreTotal": 532.5, "score1": 289.5, "score2": 104, "score3": 97, "score4": 106, "officialRemark": "未招满" },
77
+        { "source": "闵行区2.jpg", "row": 70, "officialCode": "175025", "schoolName": "上海松江区爱菊学校", "scoreTotal": 632, "score1": 362.5, "score2": 100, "score3": 125, "score4": 132 },
78
+        { "source": "闵行区2.jpg", "row": 71, "officialCode": "184006", "schoolName": "上海青浦区世外高级中学", "scoreTotal": 507, "score1": 271, "score2": 68, "score3": 79, "score4": 100, "officialRemark": "未招满" },
79
+        { "source": "闵行区2.jpg", "row": 72, "officialCode": "184007", "schoolName": "上海青浦区宏润博源高级中学", "scoreTotal": 500.5, "score1": 287, "score2": 76, "score3": 122, "score4": 82, "officialRemark": "未招满" },
80
+        { "source": "闵行区2.jpg", "row": 73, "officialCode": "185008", "schoolName": "上海宋庆龄学校", "scoreTotal": 626.5, "score1": 375.5, "score2": 132, "score3": 113, "score4": 119 },
81
+        { "source": "闵行区2.jpg", "row": 74, "officialCode": "185014", "schoolName": "上海青浦区协和双语学校", "scoreTotal": 515, "score1": 288, "score2": 95, "score3": 108, "score4": 94, "officialRemark": "未招满" },
82
+        { "source": "闵行区2.jpg", "row": 75, "officialCode": "204020", "schoolName": "上海美达菲双语高级中学", "scoreTotal": 514.5, "score1": 270, "score2": 105, "score3": 99, "score4": 107, "officialRemark": "未招满" },
83
+        { "source": "闵行区2.jpg", "row": 76, "officialCode": "204021", "schoolName": "上海奉贤区博华高级中学", "scoreTotal": 578, "score1": 339, "score2": 95, "score3": 120, "score4": 103 },
84
+        { "source": "闵行区2.jpg", "row": 77, "officialCode": "514001", "schoolName": "上海市崇明区城桥中学", "scoreTotal": 554.5, "score1": 317, "score2": 90, "score3": 121, "score4": 103 },
85
+        { "source": "闵行区2.jpg", "row": 78, "officialCode": "514004", "schoolName": "上海市崇明区堡镇中学", "scoreTotal": 607, "score1": 354, "score2": 103, "score3": 114, "score4": 119 },
86
+        { "source": "闵行区2.jpg", "row": 79, "officialCode": "514010", "schoolName": "上海民办民一中学", "scoreTotal": 490, "score1": 257, "score2": 91, "score3": 121, "score4": 101 },
87
+        { "source": "闵行区2.jpg", "row": 80, "officialCode": "514014", "schoolName": "上海新纪元双语学校", "scoreTotal": 468, "score1": 272.5, "score2": 69, "score3": 80, "score4": 97, "officialRemark": "未招满" }
88
+      ]
89
+    }
90
+  ]
91
+}

Разлика између датотеке није приказан због своје велике величине
+ 1131 - 0
秒过分数线数据导入/mps_score_1_15_minhang_pdf_extracted_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 1725 - 0
秒过分数线数据导入/mps_score_1_15_minhang_pdf_verify_report_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 2170 - 0
秒过分数线数据导入/mps_score_1_15_new_source_verify_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 3780 - 0
秒过分数线数据导入/mps_score_1_15_new_source_verify_report_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 1940 - 0
秒过分数线数据导入/mps_score_1_15_pudong_scores_2026_import_report.json


+ 605 - 0
秒过分数线数据导入/mps_score_1_15_qingpu_html_extracted_2026.json

@@ -0,0 +1,605 @@
1
+{
2
+  "districts": [
3
+    {
4
+      "districtId": 14,
5
+      "district": "青浦区",
6
+      "source": "青浦区.html",
7
+      "rows": [
8
+        {
9
+          "source": "青浦区.html",
10
+          "row": 1,
11
+          "schoolName": "上海市上海中学",
12
+          "scoreTotal": 718.5,
13
+          "score1": 426.5,
14
+          "score2": 147,
15
+          "score3": 137,
16
+          "score4": 144,
17
+          "officialRemark": ""
18
+        },
19
+        {
20
+          "source": "青浦区.html",
21
+          "row": 2,
22
+          "schoolName": "上海田家炳中学",
23
+          "scoreTotal": 639,
24
+          "score1": 366,
25
+          "score2": 120,
26
+          "score3": 116,
27
+          "score4": 131,
28
+          "officialRemark": ""
29
+        },
30
+        {
31
+          "source": "青浦区.html",
32
+          "row": 3,
33
+          "schoolName": "上海戏剧学院附属高级中学(艺术班)",
34
+          "scoreTotal": 609.5,
35
+          "score1": 354.5,
36
+          "score2": 100,
37
+          "score3": 116,
38
+          "score4": 116,
39
+          "officialRemark": ""
40
+        },
41
+        {
42
+          "source": "青浦区.html",
43
+          "row": 4,
44
+          "schoolName": "上海安生学校",
45
+          "scoreTotal": 0,
46
+          "score1": 0,
47
+          "score2": 0,
48
+          "score3": 0,
49
+          "score4": 0,
50
+          "officialRemark": "未录满"
51
+        },
52
+        {
53
+          "source": "青浦区.html",
54
+          "row": 5,
55
+          "schoolName": "上海音乐学院附属安师实验中学(艺术班)",
56
+          "scoreTotal": 631,
57
+          "score1": 375,
58
+          "score2": 116,
59
+          "score3": 120,
60
+          "score4": 112,
61
+          "officialRemark": ""
62
+        },
63
+        {
64
+          "source": "青浦区.html",
65
+          "row": 6,
66
+          "schoolName": "上海交通大学附属中学",
67
+          "scoreTotal": 716.5,
68
+          "score1": 422.5,
69
+          "score2": 148,
70
+          "score3": 127,
71
+          "score4": 145,
72
+          "officialRemark": ""
73
+        },
74
+        {
75
+          "source": "青浦区.html",
76
+          "row": 7,
77
+          "schoolName": "复旦大学附属中学",
78
+          "scoreTotal": 715.5,
79
+          "score1": 422.5,
80
+          "score2": 143,
81
+          "score3": 131,
82
+          "score4": 147,
83
+          "officialRemark": ""
84
+        },
85
+        {
86
+          "source": "青浦区.html",
87
+          "row": 8,
88
+          "schoolName": "上海市民办燎原双语高级中学",
89
+          "scoreTotal": 574.5,
90
+          "score1": 347,
91
+          "score2": 94,
92
+          "score3": 116,
93
+          "score4": 92,
94
+          "officialRemark": ""
95
+        },
96
+        {
97
+          "source": "青浦区.html",
98
+          "row": 9,
99
+          "schoolName": "上海圣华紫竹高级中学",
100
+          "scoreTotal": 0,
101
+          "score1": 0,
102
+          "score2": 0,
103
+          "score3": 0,
104
+          "score4": 0,
105
+          "officialRemark": "未录满"
106
+        },
107
+        {
108
+          "source": "青浦区.html",
109
+          "row": 10,
110
+          "schoolName": "上海闵行区诺达双语学校",
111
+          "scoreTotal": 0,
112
+          "score1": 0,
113
+          "score2": 0,
114
+          "score3": 0,
115
+          "score4": 0,
116
+          "officialRemark": "未录满"
117
+        },
118
+        {
119
+          "source": "青浦区.html",
120
+          "row": 11,
121
+          "schoolName": "上海民办行中中学",
122
+          "scoreTotal": 480,
123
+          "score1": 253,
124
+          "score2": 94,
125
+          "score3": 109,
126
+          "score4": 97,
127
+          "officialRemark": "未录满"
128
+        },
129
+        {
130
+          "source": "青浦区.html",
131
+          "row": 12,
132
+          "schoolName": "上海存志高级中学",
133
+          "scoreTotal": 641.5,
134
+          "score1": 373.5,
135
+          "score2": 113,
136
+          "score3": 121,
137
+          "score4": 128,
138
+          "officialRemark": ""
139
+        },
140
+        {
141
+          "source": "青浦区.html",
142
+          "row": 13,
143
+          "schoolName": "上海创艺高级中学",
144
+          "scoreTotal": 586,
145
+          "score1": 347.5,
146
+          "score2": 89,
147
+          "score3": 130,
148
+          "score4": 99.5,
149
+          "officialRemark": "未录满"
150
+        },
151
+        {
152
+          "source": "青浦区.html",
153
+          "row": 14,
154
+          "schoolName": "上海市宝山华曜高级中学",
155
+          "scoreTotal": 514,
156
+          "score1": 299,
157
+          "score2": 90,
158
+          "score3": 91,
159
+          "score4": 81,
160
+          "officialRemark": "未录满"
161
+        },
162
+        {
163
+          "source": "青浦区.html",
164
+          "row": 15,
165
+          "schoolName": "上海市同洲模范学校",
166
+          "scoreTotal": 503.5,
167
+          "score1": 285,
168
+          "score2": 81,
169
+          "score3": 111,
170
+          "score4": 91,
171
+          "officialRemark": "未录满"
172
+        },
173
+        {
174
+          "source": "青浦区.html",
175
+          "row": 16,
176
+          "schoolName": "上海金瑞学校",
177
+          "scoreTotal": 0,
178
+          "score1": 0,
179
+          "score2": 0,
180
+          "score3": 0,
181
+          "score4": 0,
182
+          "officialRemark": "未录满"
183
+        },
184
+        {
185
+          "source": "青浦区.html",
186
+          "row": 17,
187
+          "schoolName": "上海宝山区世外学校",
188
+          "scoreTotal": 681,
189
+          "score1": 399,
190
+          "score2": 126,
191
+          "score3": 126,
192
+          "score4": 137,
193
+          "officialRemark": ""
194
+        },
195
+        {
196
+          "source": "青浦区.html",
197
+          "row": 18,
198
+          "schoolName": "上海市民办远东学校",
199
+          "scoreTotal": 529,
200
+          "score1": 298,
201
+          "score2": 109,
202
+          "score3": 99,
203
+          "score4": 97,
204
+          "officialRemark": ""
205
+        },
206
+        {
207
+          "source": "青浦区.html",
208
+          "row": 19,
209
+          "schoolName": "上海华旭双语学校",
210
+          "scoreTotal": 0,
211
+          "score1": 0,
212
+          "score2": 0,
213
+          "score3": 0,
214
+          "score4": 0,
215
+          "officialRemark": "未录满"
216
+        },
217
+        {
218
+          "source": "青浦区.html",
219
+          "row": 20,
220
+          "schoolName": "上海嘉定区民办华盛怀少学校",
221
+          "scoreTotal": 0,
222
+          "score1": 0,
223
+          "score2": 0,
224
+          "score3": 0,
225
+          "score4": 0,
226
+          "officialRemark": "未录满"
227
+        },
228
+        {
229
+          "source": "青浦区.html",
230
+          "row": 21,
231
+          "schoolName": "华东师范大学第二附属中学",
232
+          "scoreTotal": 715,
233
+          "score1": 424,
234
+          "score2": 143,
235
+          "score3": 135,
236
+          "score4": 146,
237
+          "officialRemark": ""
238
+        },
239
+        {
240
+          "source": "青浦区.html",
241
+          "row": 22,
242
+          "schoolName": "上海师范大学附属中学",
243
+          "scoreTotal": 695.5,
244
+          "score1": 404.5,
245
+          "score2": 135,
246
+          "score3": 128,
247
+          "score4": 143,
248
+          "officialRemark": ""
249
+        },
250
+        {
251
+          "source": "青浦区.html",
252
+          "row": 23,
253
+          "schoolName": "上海市浦东新区民办浦实高级中学",
254
+          "scoreTotal": 527.5,
255
+          "score1": 285.5,
256
+          "score2": 87,
257
+          "score3": 113,
258
+          "score4": 112,
259
+          "officialRemark": "未录满"
260
+        },
261
+        {
262
+          "source": "青浦区.html",
263
+          "row": 24,
264
+          "schoolName": "上海市民办丰华高级中学",
265
+          "scoreTotal": 538,
266
+          "score1": 272,
267
+          "score2": 87,
268
+          "score3": 99,
269
+          "score4": 126,
270
+          "officialRemark": ""
271
+        },
272
+        {
273
+          "source": "青浦区.html",
274
+          "row": 25,
275
+          "schoolName": "民办上海工商外国语职业学院附属中学",
276
+          "scoreTotal": 552,
277
+          "score1": 315.5,
278
+          "score2": 93,
279
+          "score3": 119,
280
+          "score4": 104,
281
+          "officialRemark": ""
282
+        },
283
+        {
284
+          "source": "青浦区.html",
285
+          "row": 26,
286
+          "schoolName": "上海浦东新区民办东鼎外国语学校",
287
+          "scoreTotal": 579.5,
288
+          "score1": 339.5,
289
+          "score2": 107,
290
+          "score3": 109,
291
+          "score4": 104,
292
+          "officialRemark": ""
293
+        },
294
+        {
295
+          "source": "青浦区.html",
296
+          "row": 27,
297
+          "schoolName": "上海市民办尚德实验学校",
298
+          "scoreTotal": 586.5,
299
+          "score1": 343.5,
300
+          "score2": 96,
301
+          "score3": 119,
302
+          "score4": 113,
303
+          "officialRemark": ""
304
+        },
305
+        {
306
+          "source": "青浦区.html",
307
+          "row": 28,
308
+          "schoolName": "上海市民办交大南洋中学",
309
+          "scoreTotal": 538,
310
+          "score1": 303,
311
+          "score2": 67,
312
+          "score3": 125,
313
+          "score4": 102,
314
+          "officialRemark": ""
315
+        },
316
+        {
317
+          "source": "青浦区.html",
318
+          "row": 29,
319
+          "schoolName": "上海市民办永昌中学",
320
+          "scoreTotal": 527.5,
321
+          "score1": 304.5,
322
+          "score2": 67,
323
+          "score3": 110,
324
+          "score4": 95,
325
+          "officialRemark": "未录满"
326
+        },
327
+        {
328
+          "source": "青浦区.html",
329
+          "row": 30,
330
+          "schoolName": "上海金山区世外学校",
331
+          "scoreTotal": 630.5,
332
+          "score1": 375.5,
333
+          "score2": 115,
334
+          "score3": 124,
335
+          "score4": 115,
336
+          "officialRemark": ""
337
+        },
338
+        {
339
+          "source": "青浦区.html",
340
+          "row": 31,
341
+          "schoolName": "上海市松江区科德高级中学",
342
+          "scoreTotal": 526,
343
+          "score1": 289.5,
344
+          "score2": 94,
345
+          "score3": 113,
346
+          "score4": 113,
347
+          "officialRemark": "未录满"
348
+        },
349
+        {
350
+          "source": "青浦区.html",
351
+          "row": 32,
352
+          "schoolName": "上海市松江区励滕高级中学",
353
+          "scoreTotal": 0,
354
+          "score1": 0,
355
+          "score2": 0,
356
+          "score3": 0,
357
+          "score4": 0,
358
+          "officialRemark": "未录满"
359
+        },
360
+        {
361
+          "source": "青浦区.html",
362
+          "row": 33,
363
+          "schoolName": "上海市松江九峰实验学校",
364
+          "scoreTotal": 646.5,
365
+          "score1": 399,
366
+          "score2": 139,
367
+          "score3": 122,
368
+          "score4": 115,
369
+          "officialRemark": ""
370
+        },
371
+        {
372
+          "source": "青浦区.html",
373
+          "row": 34,
374
+          "schoolName": "上海领科双语学校",
375
+          "scoreTotal": 657,
376
+          "score1": 380,
377
+          "score2": 123,
378
+          "score3": 118,
379
+          "score4": 135,
380
+          "officialRemark": "未录满"
381
+        },
382
+        {
383
+          "source": "青浦区.html",
384
+          "row": 35,
385
+          "schoolName": "上海市西外外国语学校",
386
+          "scoreTotal": 501,
387
+          "score1": 274,
388
+          "score2": 75,
389
+          "score3": 86,
390
+          "score4": 96,
391
+          "officialRemark": "未录满"
392
+        },
393
+        {
394
+          "source": "青浦区.html",
395
+          "row": 36,
396
+          "schoolName": "上海赫贤学校",
397
+          "scoreTotal": 0,
398
+          "score1": 0,
399
+          "score2": 0,
400
+          "score3": 0,
401
+          "score4": 0,
402
+          "officialRemark": "未录满"
403
+        },
404
+        {
405
+          "source": "青浦区.html",
406
+          "row": 37,
407
+          "schoolName": "上海松江区爱菊学校",
408
+          "scoreTotal": 506,
409
+          "score1": 303.5,
410
+          "score2": 80,
411
+          "score3": 110,
412
+          "score4": 78,
413
+          "officialRemark": "未录满"
414
+        },
415
+        {
416
+          "source": "青浦区.html",
417
+          "row": 38,
418
+          "schoolName": "上海师范大学附属青浦实验中学",
419
+          "scoreTotal": 626.5,
420
+          "score1": 360.5,
421
+          "score2": 119,
422
+          "score3": 125,
423
+          "score4": 127,
424
+          "officialRemark": ""
425
+        },
426
+        {
427
+          "source": "青浦区.html",
428
+          "row": 39,
429
+          "schoolName": "上海市青浦高级中学",
430
+          "scoreTotal": 671.5,
431
+          "score1": 395,
432
+          "score2": 133,
433
+          "score3": 118,
434
+          "score4": 131,
435
+          "officialRemark": ""
436
+        },
437
+        {
438
+          "source": "青浦区.html",
439
+          "row": 40,
440
+          "schoolName": "复旦大学附属中学青浦分校",
441
+          "scoreTotal": 695,
442
+          "score1": 412,
443
+          "score2": 137,
444
+          "score3": 129,
445
+          "score4": 138,
446
+          "officialRemark": ""
447
+        },
448
+        {
449
+          "source": "青浦区.html",
450
+          "row": 41,
451
+          "schoolName": "上海市朱家角中学",
452
+          "scoreTotal": 655.5,
453
+          "score1": 379.5,
454
+          "score2": 117,
455
+          "score3": 119,
456
+          "score4": 133,
457
+          "officialRemark": ""
458
+        },
459
+        {
460
+          "source": "青浦区.html",
461
+          "row": 42,
462
+          "schoolName": "上海市青浦区第二中学",
463
+          "scoreTotal": 555.5,
464
+          "score1": 322.5,
465
+          "score2": 93,
466
+          "score3": 106,
467
+          "score4": 100,
468
+          "officialRemark": ""
469
+        },
470
+        {
471
+          "source": "青浦区.html",
472
+          "row": 43,
473
+          "schoolName": "上海市青浦区东湖中学",
474
+          "scoreTotal": 599,
475
+          "score1": 365,
476
+          "score2": 112,
477
+          "score3": 123,
478
+          "score4": 92,
479
+          "officialRemark": ""
480
+        },
481
+        {
482
+          "source": "青浦区.html",
483
+          "row": 44,
484
+          "schoolName": "上海市青浦区第一中学",
485
+          "scoreTotal": 632,
486
+          "score1": 377,
487
+          "score2": 125,
488
+          "score3": 120,
489
+          "score4": 117,
490
+          "officialRemark": ""
491
+        },
492
+        {
493
+          "source": "青浦区.html",
494
+          "row": 45,
495
+          "schoolName": "上海青浦区世外高级中学",
496
+          "scoreTotal": 545,
497
+          "score1": 294,
498
+          "score2": 90,
499
+          "score3": 113,
500
+          "score4": 110,
501
+          "officialRemark": "未录满"
502
+        },
503
+        {
504
+          "source": "青浦区.html",
505
+          "row": 46,
506
+          "schoolName": "上海青浦区宏润博源高级中学",
507
+          "scoreTotal": 493.5,
508
+          "score1": 280.5,
509
+          "score2": 88,
510
+          "score3": 105,
511
+          "score4": 86,
512
+          "officialRemark": "未录满"
513
+        },
514
+        {
515
+          "source": "青浦区.html",
516
+          "row": 47,
517
+          "schoolName": "上海宋庆龄学校",
518
+          "scoreTotal": 614.5,
519
+          "score1": 358.5,
520
+          "score2": 101,
521
+          "score3": 112,
522
+          "score4": 118,
523
+          "officialRemark": ""
524
+        },
525
+        {
526
+          "source": "青浦区.html",
527
+          "row": 48,
528
+          "schoolName": "上海青浦区协和双语学校",
529
+          "scoreTotal": 530.5,
530
+          "score1": 304.5,
531
+          "score2": 95,
532
+          "score3": 102,
533
+          "score4": 94,
534
+          "officialRemark": "未录满"
535
+        },
536
+        {
537
+          "source": "青浦区.html",
538
+          "row": 49,
539
+          "schoolName": "上海美达菲双语高级中学",
540
+          "scoreTotal": 529.5,
541
+          "score1": 295.5,
542
+          "score2": 83,
543
+          "score3": 113,
544
+          "score4": 102,
545
+          "officialRemark": "未录满"
546
+        },
547
+        {
548
+          "source": "青浦区.html",
549
+          "row": 50,
550
+          "schoolName": "上海奉贤区博华高级中学",
551
+          "scoreTotal": 507.5,
552
+          "score1": 277.5,
553
+          "score2": 93,
554
+          "score3": 91,
555
+          "score4": 99,
556
+          "officialRemark": ""
557
+        },
558
+        {
559
+          "source": "青浦区.html",
560
+          "row": 51,
561
+          "schoolName": "上海市崇明区城桥中学",
562
+          "scoreTotal": 574.5,
563
+          "score1": 325.5,
564
+          "score2": 100,
565
+          "score3": 114,
566
+          "score4": 110,
567
+          "officialRemark": ""
568
+        },
569
+        {
570
+          "source": "青浦区.html",
571
+          "row": 52,
572
+          "schoolName": "上海市崇明区堡镇中学",
573
+          "scoreTotal": 579,
574
+          "score1": 323,
575
+          "score2": 98,
576
+          "score3": 119,
577
+          "score4": 116,
578
+          "officialRemark": ""
579
+        },
580
+        {
581
+          "source": "青浦区.html",
582
+          "row": 53,
583
+          "schoolName": "上海民办民一中学",
584
+          "scoreTotal": 451.5,
585
+          "score1": 227.5,
586
+          "score2": 105,
587
+          "score3": 88,
588
+          "score4": 95,
589
+          "officialRemark": ""
590
+        },
591
+        {
592
+          "source": "青浦区.html",
593
+          "row": 54,
594
+          "schoolName": "上海新纪元双语学校",
595
+          "scoreTotal": 0,
596
+          "score1": 0,
597
+          "score2": 0,
598
+          "score3": 0,
599
+          "score4": 0,
600
+          "officialRemark": "未录满"
601
+        }
602
+      ]
603
+    }
604
+  ]
605
+}

Разлика између датотеке није приказан због своје велике величине
+ 1179 - 0
秒过分数线数据导入/mps_score_1_15_qingpu_html_verify_report_2026.json


+ 991 - 0
秒过分数线数据导入/mps_score_1_15_qingpu_scores_2026_import_report.json

@@ -0,0 +1,991 @@
1
+{
2
+  "dry_run": false,
3
+  "source_image": "/Volumes/程杰外接SD盘/上海中考招生计划/2026/分数线/1-15志愿/青浦区1.jpg",
4
+  "summary": {
5
+    "district_id": 14,
6
+    "district": "青浦区",
7
+    "score_type": "1-15志愿",
8
+    "official_rows": 54,
9
+    "official_numeric_rows": 45,
10
+    "official_no_admission_rows": 9,
11
+    "db_rows": 54,
12
+    "not_in_official_image_rows": 0,
13
+    "min_score": 451.5,
14
+    "max_score": 718.5
15
+  },
16
+  "updates": [
17
+    {
18
+      "id": 39602,
19
+      "page": 1,
20
+      "row": 1,
21
+      "schoolTarget": "1",
22
+      "schoolName": "上海市上海中学",
23
+      "schoolTargetRemark": "",
24
+      "PlanNum": 1,
25
+      "noAdmission": false,
26
+      "officialRemark": "",
27
+      "ScoreTotal": 718.5,
28
+      "Score1": 426.5,
29
+      "Score2": 147,
30
+      "Score3": 137,
31
+      "Score4": 144,
32
+      "ScoreTotalDifferenceValue": 7.5,
33
+      "previousScoreTotal": 711
34
+    },
35
+    {
36
+      "id": 39607,
37
+      "page": 1,
38
+      "row": 2,
39
+      "schoolTarget": "108",
40
+      "schoolName": "上海田家炳中学",
41
+      "schoolTargetRemark": "",
42
+      "PlanNum": 4,
43
+      "noAdmission": false,
44
+      "officialRemark": "",
45
+      "ScoreTotal": 639,
46
+      "Score1": 366,
47
+      "Score2": 120,
48
+      "Score3": 116,
49
+      "Score4": 131,
50
+      "ScoreTotalDifferenceValue": -4,
51
+      "previousScoreTotal": 643
52
+    },
53
+    {
54
+      "id": 39608,
55
+      "page": 1,
56
+      "row": 3,
57
+      "schoolTarget": "100",
58
+      "schoolName": "上海戏剧学院附属高级中学",
59
+      "schoolTargetRemark": "(艺术班)",
60
+      "PlanNum": 1,
61
+      "noAdmission": false,
62
+      "officialRemark": "",
63
+      "ScoreTotal": 609.5,
64
+      "Score1": 354.5,
65
+      "Score2": 100,
66
+      "Score3": 116,
67
+      "Score4": 116,
68
+      "ScoreTotalDifferenceValue": 10,
69
+      "previousScoreTotal": 599.5
70
+    },
71
+    {
72
+      "id": 39610,
73
+      "page": 1,
74
+      "row": 4,
75
+      "schoolTarget": "122",
76
+      "schoolName": "上海安生学校",
77
+      "schoolTargetRemark": "",
78
+      "PlanNum": 2,
79
+      "noAdmission": true,
80
+      "officialRemark": "未录满",
81
+      "ScoreTotal": 0,
82
+      "Score1": 0,
83
+      "Score2": 0,
84
+      "Score3": 0,
85
+      "Score4": 0,
86
+      "ScoreTotalDifferenceValue": null,
87
+      "previousScoreTotal": null
88
+    },
89
+    {
90
+      "id": 39609,
91
+      "page": 1,
92
+      "row": 5,
93
+      "schoolTarget": "117",
94
+      "schoolName": "上海音乐学院附属安师实验中学",
95
+      "schoolTargetRemark": "(艺术班)",
96
+      "PlanNum": 1,
97
+      "noAdmission": false,
98
+      "officialRemark": "",
99
+      "ScoreTotal": 631,
100
+      "Score1": 375,
101
+      "Score2": 116,
102
+      "Score3": 120,
103
+      "Score4": 112,
104
+      "ScoreTotalDifferenceValue": 39.5,
105
+      "previousScoreTotal": 591.5
106
+    },
107
+    {
108
+      "id": 39605,
109
+      "page": 1,
110
+      "row": 6,
111
+      "schoolTarget": "2",
112
+      "schoolName": "上海交通大学附属中学",
113
+      "schoolTargetRemark": "",
114
+      "PlanNum": 1,
115
+      "noAdmission": false,
116
+      "officialRemark": "",
117
+      "ScoreTotal": 716.5,
118
+      "Score1": 422.5,
119
+      "Score2": 148,
120
+      "Score3": 127,
121
+      "Score4": 145,
122
+      "ScoreTotalDifferenceValue": 9,
123
+      "previousScoreTotal": 707.5
124
+    },
125
+    {
126
+      "id": 39604,
127
+      "page": 1,
128
+      "row": 7,
129
+      "schoolTarget": "3",
130
+      "schoolName": "复旦大学附属中学",
131
+      "schoolTargetRemark": "",
132
+      "PlanNum": 1,
133
+      "noAdmission": false,
134
+      "officialRemark": "",
135
+      "ScoreTotal": 715.5,
136
+      "Score1": 422.5,
137
+      "Score2": 143,
138
+      "Score3": 131,
139
+      "Score4": 147,
140
+      "ScoreTotalDifferenceValue": 7.5,
141
+      "previousScoreTotal": 708
142
+    },
143
+    {
144
+      "id": 39613,
145
+      "page": 1,
146
+      "row": 8,
147
+      "schoolTarget": "156",
148
+      "schoolName": "上海市民办燎原双语高级中学",
149
+      "schoolTargetRemark": "",
150
+      "PlanNum": 2,
151
+      "noAdmission": false,
152
+      "officialRemark": "",
153
+      "ScoreTotal": 574.5,
154
+      "Score1": 347,
155
+      "Score2": 94,
156
+      "Score3": 116,
157
+      "Score4": 92,
158
+      "ScoreTotalDifferenceValue": 7,
159
+      "previousScoreTotal": 567.5
160
+    },
161
+    {
162
+      "id": 39611,
163
+      "page": 1,
164
+      "row": 9,
165
+      "schoolTarget": "1026",
166
+      "schoolName": "上海圣华紫竹高级中学",
167
+      "schoolTargetRemark": "",
168
+      "PlanNum": 3,
169
+      "noAdmission": true,
170
+      "officialRemark": "未录满",
171
+      "ScoreTotal": 0,
172
+      "Score1": 0,
173
+      "Score2": 0,
174
+      "Score3": 0,
175
+      "Score4": 0,
176
+      "ScoreTotalDifferenceValue": null,
177
+      "previousScoreTotal": null
178
+    },
179
+    {
180
+      "id": 39612,
181
+      "page": 1,
182
+      "row": 10,
183
+      "schoolTarget": "157",
184
+      "schoolName": "上海闵行区诺达双语学校",
185
+      "schoolTargetRemark": "",
186
+      "PlanNum": 2,
187
+      "noAdmission": true,
188
+      "officialRemark": "未录满",
189
+      "ScoreTotal": 0,
190
+      "Score1": 0,
191
+      "Score2": 0,
192
+      "Score3": 0,
193
+      "Score4": 0,
194
+      "ScoreTotalDifferenceValue": null,
195
+      "previousScoreTotal": null
196
+    },
197
+    {
198
+      "id": 39619,
199
+      "page": 1,
200
+      "row": 11,
201
+      "schoolTarget": "851",
202
+      "schoolName": "上海民办行中中学",
203
+      "schoolTargetRemark": "",
204
+      "PlanNum": 8,
205
+      "noAdmission": false,
206
+      "officialRemark": "未录满",
207
+      "ScoreTotal": 480,
208
+      "Score1": 253,
209
+      "Score2": 94,
210
+      "Score3": 109,
211
+      "Score4": 97,
212
+      "ScoreTotalDifferenceValue": -40,
213
+      "previousScoreTotal": 520
214
+    },
215
+    {
216
+      "id": 39616,
217
+      "page": 1,
218
+      "row": 12,
219
+      "schoolTarget": "177",
220
+      "schoolName": "上海存志高级中学",
221
+      "schoolTargetRemark": "",
222
+      "PlanNum": 2,
223
+      "noAdmission": false,
224
+      "officialRemark": "",
225
+      "ScoreTotal": 641.5,
226
+      "Score1": 373.5,
227
+      "Score2": 113,
228
+      "Score3": 121,
229
+      "Score4": 128,
230
+      "ScoreTotalDifferenceValue": 69,
231
+      "previousScoreTotal": 572.5
232
+    },
233
+    {
234
+      "id": 39617,
235
+      "page": 1,
236
+      "row": 13,
237
+      "schoolTarget": "179",
238
+      "schoolName": "上海创艺高级中学",
239
+      "schoolTargetRemark": "",
240
+      "PlanNum": 2,
241
+      "noAdmission": false,
242
+      "officialRemark": "未录满",
243
+      "ScoreTotal": 586,
244
+      "Score1": 347.5,
245
+      "Score2": 89,
246
+      "Score3": 130,
247
+      "Score4": 99.5,
248
+      "ScoreTotalDifferenceValue": 47.5,
249
+      "previousScoreTotal": 538.5
250
+    },
251
+    {
252
+      "id": 39615,
253
+      "page": 1,
254
+      "row": 14,
255
+      "schoolTarget": "180",
256
+      "schoolName": "上海市宝山华曜高级中学",
257
+      "schoolTargetRemark": "",
258
+      "PlanNum": 12,
259
+      "noAdmission": false,
260
+      "officialRemark": "未录满",
261
+      "ScoreTotal": 514,
262
+      "Score1": 299,
263
+      "Score2": 90,
264
+      "Score3": 91,
265
+      "Score4": 81,
266
+      "ScoreTotalDifferenceValue": -88,
267
+      "previousScoreTotal": 602
268
+    },
269
+    {
270
+      "id": 39618,
271
+      "page": 1,
272
+      "row": 15,
273
+      "schoolTarget": "175",
274
+      "schoolName": "上海市同洲模范学校",
275
+      "schoolTargetRemark": "",
276
+      "PlanNum": 12,
277
+      "noAdmission": false,
278
+      "officialRemark": "未录满",
279
+      "ScoreTotal": 503.5,
280
+      "Score1": 285,
281
+      "Score2": 81,
282
+      "Score3": 111,
283
+      "Score4": 91,
284
+      "ScoreTotalDifferenceValue": -31,
285
+      "previousScoreTotal": 534.5
286
+    },
287
+    {
288
+      "id": 39620,
289
+      "page": 1,
290
+      "row": 16,
291
+      "schoolTarget": "852",
292
+      "schoolName": "上海金瑞学校",
293
+      "schoolTargetRemark": "",
294
+      "PlanNum": 1,
295
+      "noAdmission": true,
296
+      "officialRemark": "未录满",
297
+      "ScoreTotal": 0,
298
+      "Score1": 0,
299
+      "Score2": 0,
300
+      "Score3": 0,
301
+      "Score4": 0,
302
+      "ScoreTotalDifferenceValue": null,
303
+      "previousScoreTotal": null
304
+    },
305
+    {
306
+      "id": 39614,
307
+      "page": 1,
308
+      "row": 17,
309
+      "schoolTarget": "176",
310
+      "schoolName": "上海宝山区世外学校",
311
+      "schoolTargetRemark": "",
312
+      "PlanNum": 8,
313
+      "noAdmission": false,
314
+      "officialRemark": "",
315
+      "ScoreTotal": 681,
316
+      "Score1": 399,
317
+      "Score2": 126,
318
+      "Score3": 126,
319
+      "Score4": 137,
320
+      "ScoreTotalDifferenceValue": 6,
321
+      "previousScoreTotal": 675
322
+    },
323
+    {
324
+      "id": 39621,
325
+      "page": 1,
326
+      "row": 18,
327
+      "schoolTarget": "188",
328
+      "schoolName": "上海市民办远东学校",
329
+      "schoolTargetRemark": "",
330
+      "PlanNum": 5,
331
+      "noAdmission": false,
332
+      "officialRemark": "",
333
+      "ScoreTotal": 529,
334
+      "Score1": 298,
335
+      "Score2": 109,
336
+      "Score3": 99,
337
+      "Score4": 97,
338
+      "ScoreTotalDifferenceValue": -32,
339
+      "previousScoreTotal": 561
340
+    },
341
+    {
342
+      "id": 39622,
343
+      "page": 1,
344
+      "row": 19,
345
+      "schoolTarget": "189",
346
+      "schoolName": "上海华旭双语学校",
347
+      "schoolTargetRemark": "",
348
+      "PlanNum": 22,
349
+      "noAdmission": true,
350
+      "officialRemark": "未录满",
351
+      "ScoreTotal": 0,
352
+      "Score1": 0,
353
+      "Score2": 0,
354
+      "Score3": 0,
355
+      "Score4": 0,
356
+      "ScoreTotalDifferenceValue": null,
357
+      "previousScoreTotal": null
358
+    },
359
+    {
360
+      "id": 39623,
361
+      "page": 1,
362
+      "row": 20,
363
+      "schoolTarget": "190",
364
+      "schoolName": "上海嘉定区民办华盛怀少学校",
365
+      "schoolTargetRemark": "",
366
+      "PlanNum": 6,
367
+      "noAdmission": true,
368
+      "officialRemark": "未录满",
369
+      "ScoreTotal": 0,
370
+      "Score1": 0,
371
+      "Score2": 0,
372
+      "Score3": 0,
373
+      "Score4": 0,
374
+      "ScoreTotalDifferenceValue": null,
375
+      "previousScoreTotal": null
376
+    },
377
+    {
378
+      "id": 39603,
379
+      "page": 1,
380
+      "row": 21,
381
+      "schoolTarget": "4",
382
+      "schoolName": "华东师范大学第二附属中学",
383
+      "schoolTargetRemark": "",
384
+      "PlanNum": 1,
385
+      "noAdmission": false,
386
+      "officialRemark": "",
387
+      "ScoreTotal": 715,
388
+      "Score1": 424,
389
+      "Score2": 143,
390
+      "Score3": 135,
391
+      "Score4": 146,
392
+      "ScoreTotalDifferenceValue": 8,
393
+      "previousScoreTotal": 707
394
+    },
395
+    {
396
+      "id": 39606,
397
+      "page": 1,
398
+      "row": 22,
399
+      "schoolTarget": "5",
400
+      "schoolName": "上海师范大学附属中学",
401
+      "schoolTargetRemark": "",
402
+      "PlanNum": 1,
403
+      "noAdmission": false,
404
+      "officialRemark": "",
405
+      "ScoreTotal": 695.5,
406
+      "Score1": 404.5,
407
+      "Score2": 135,
408
+      "Score3": 128,
409
+      "Score4": 143,
410
+      "ScoreTotalDifferenceValue": -3.5,
411
+      "previousScoreTotal": 699
412
+    },
413
+    {
414
+      "id": 39626,
415
+      "page": 1,
416
+      "row": 23,
417
+      "schoolTarget": "218",
418
+      "schoolName": "上海市浦东新区民办浦实高级中学",
419
+      "schoolTargetRemark": "",
420
+      "PlanNum": 2,
421
+      "noAdmission": false,
422
+      "officialRemark": "未录满",
423
+      "ScoreTotal": 527.5,
424
+      "Score1": 285.5,
425
+      "Score2": 87,
426
+      "Score3": 113,
427
+      "Score4": 112,
428
+      "ScoreTotalDifferenceValue": -35.5,
429
+      "previousScoreTotal": 563
430
+    },
431
+    {
432
+      "id": 39627,
433
+      "page": 1,
434
+      "row": 24,
435
+      "schoolTarget": "220",
436
+      "schoolName": "上海市民办丰华高级中学",
437
+      "schoolTargetRemark": "",
438
+      "PlanNum": 3,
439
+      "noAdmission": false,
440
+      "officialRemark": "",
441
+      "ScoreTotal": 538,
442
+      "Score1": 272,
443
+      "Score2": 87,
444
+      "Score3": 99,
445
+      "Score4": 126,
446
+      "ScoreTotalDifferenceValue": -25,
447
+      "previousScoreTotal": 563
448
+    },
449
+    {
450
+      "id": 39628,
451
+      "page": 1,
452
+      "row": 25,
453
+      "schoolTarget": "223",
454
+      "schoolName": "民办上海工商外国语职业学院附属中学",
455
+      "schoolTargetRemark": "",
456
+      "PlanNum": 1,
457
+      "noAdmission": false,
458
+      "officialRemark": "",
459
+      "ScoreTotal": 552,
460
+      "Score1": 315.5,
461
+      "Score2": 93,
462
+      "Score3": 119,
463
+      "Score4": 104,
464
+      "ScoreTotalDifferenceValue": null,
465
+      "previousScoreTotal": null
466
+    },
467
+    {
468
+      "id": 39625,
469
+      "page": 1,
470
+      "row": 26,
471
+      "schoolTarget": "229",
472
+      "schoolName": "上海浦东新区民办东鼎外国语学校",
473
+      "schoolTargetRemark": "",
474
+      "PlanNum": 1,
475
+      "noAdmission": false,
476
+      "officialRemark": "",
477
+      "ScoreTotal": 579.5,
478
+      "Score1": 339.5,
479
+      "Score2": 107,
480
+      "Score3": 109,
481
+      "Score4": 104,
482
+      "ScoreTotalDifferenceValue": 5,
483
+      "previousScoreTotal": 574.5
484
+    },
485
+    {
486
+      "id": 39624,
487
+      "page": 1,
488
+      "row": 27,
489
+      "schoolTarget": "230",
490
+      "schoolName": "上海市民办尚德实验学校",
491
+      "schoolTargetRemark": "",
492
+      "PlanNum": 4,
493
+      "noAdmission": false,
494
+      "officialRemark": "",
495
+      "ScoreTotal": 586.5,
496
+      "Score1": 343.5,
497
+      "Score2": 96,
498
+      "Score3": 119,
499
+      "Score4": 113,
500
+      "ScoreTotalDifferenceValue": -24.5,
501
+      "previousScoreTotal": 611
502
+    },
503
+    {
504
+      "id": 39630,
505
+      "page": 1,
506
+      "row": 28,
507
+      "schoolTarget": "243",
508
+      "schoolName": "上海市民办交大南洋中学",
509
+      "schoolTargetRemark": "",
510
+      "PlanNum": 10,
511
+      "noAdmission": false,
512
+      "officialRemark": "",
513
+      "ScoreTotal": 538,
514
+      "Score1": 303,
515
+      "Score2": 67,
516
+      "Score3": 125,
517
+      "Score4": 102,
518
+      "ScoreTotalDifferenceValue": -25.5,
519
+      "previousScoreTotal": 563.5
520
+    },
521
+    {
522
+      "id": 39631,
523
+      "page": 1,
524
+      "row": 29,
525
+      "schoolTarget": "246",
526
+      "schoolName": "上海市民办永昌中学",
527
+      "schoolTargetRemark": "",
528
+      "PlanNum": 2,
529
+      "noAdmission": false,
530
+      "officialRemark": "未录满",
531
+      "ScoreTotal": 527.5,
532
+      "Score1": 304.5,
533
+      "Score2": 67,
534
+      "Score3": 110,
535
+      "Score4": 95,
536
+      "ScoreTotalDifferenceValue": -13,
537
+      "previousScoreTotal": 540.5
538
+    },
539
+    {
540
+      "id": 39629,
541
+      "page": 1,
542
+      "row": 30,
543
+      "schoolTarget": "244",
544
+      "schoolName": "上海金山区世外学校",
545
+      "schoolTargetRemark": "",
546
+      "PlanNum": 5,
547
+      "noAdmission": false,
548
+      "officialRemark": "",
549
+      "ScoreTotal": 630.5,
550
+      "Score1": 375.5,
551
+      "Score2": 115,
552
+      "Score3": 124,
553
+      "Score4": 115,
554
+      "ScoreTotalDifferenceValue": 10,
555
+      "previousScoreTotal": 620.5
556
+    },
557
+    {
558
+      "id": 39634,
559
+      "page": 1,
560
+      "row": 31,
561
+      "schoolTarget": "859",
562
+      "schoolName": "上海市松江区科德高级中学",
563
+      "schoolTargetRemark": "",
564
+      "PlanNum": 4,
565
+      "noAdmission": false,
566
+      "officialRemark": "未录满",
567
+      "ScoreTotal": 526,
568
+      "Score1": 289.5,
569
+      "Score2": 94,
570
+      "Score3": 113,
571
+      "Score4": 113,
572
+      "ScoreTotalDifferenceValue": -76,
573
+      "previousScoreTotal": 602
574
+    },
575
+    {
576
+      "id": 39638,
577
+      "page": 1,
578
+      "row": 32,
579
+      "schoolTarget": "1034",
580
+      "schoolName": "上海市松江区励滕高级中学",
581
+      "schoolTargetRemark": "",
582
+      "PlanNum": 2,
583
+      "noAdmission": true,
584
+      "officialRemark": "未录满",
585
+      "ScoreTotal": 0,
586
+      "Score1": 0,
587
+      "Score2": 0,
588
+      "Score3": 0,
589
+      "Score4": 0,
590
+      "ScoreTotalDifferenceValue": null,
591
+      "previousScoreTotal": null
592
+    },
593
+    {
594
+      "id": 39632,
595
+      "page": 1,
596
+      "row": 33,
597
+      "schoolTarget": "255",
598
+      "schoolName": "上海市松江九峰实验学校",
599
+      "schoolTargetRemark": "",
600
+      "PlanNum": 2,
601
+      "noAdmission": false,
602
+      "officialRemark": "",
603
+      "ScoreTotal": 646.5,
604
+      "Score1": 399,
605
+      "Score2": 139,
606
+      "Score3": 122,
607
+      "Score4": 115,
608
+      "ScoreTotalDifferenceValue": -17.5,
609
+      "previousScoreTotal": 664
610
+    },
611
+    {
612
+      "id": 39633,
613
+      "page": 1,
614
+      "row": 34,
615
+      "schoolTarget": "860",
616
+      "schoolName": "上海领科双语学校",
617
+      "schoolTargetRemark": "",
618
+      "PlanNum": 2,
619
+      "noAdmission": false,
620
+      "officialRemark": "未录满",
621
+      "ScoreTotal": 657,
622
+      "Score1": 380,
623
+      "Score2": 123,
624
+      "Score3": 118,
625
+      "Score4": 135,
626
+      "ScoreTotalDifferenceValue": -5.5,
627
+      "previousScoreTotal": 662.5
628
+    },
629
+    {
630
+      "id": 39635,
631
+      "page": 1,
632
+      "row": 35,
633
+      "schoolTarget": "251",
634
+      "schoolName": "上海市西外外国语学校",
635
+      "schoolTargetRemark": "",
636
+      "PlanNum": 20,
637
+      "noAdmission": false,
638
+      "officialRemark": "未录满",
639
+      "ScoreTotal": 501,
640
+      "Score1": 274,
641
+      "Score2": 75,
642
+      "Score3": 86,
643
+      "Score4": 96,
644
+      "ScoreTotalDifferenceValue": -47.5,
645
+      "previousScoreTotal": 548.5
646
+    },
647
+    {
648
+      "id": 39637,
649
+      "page": 1,
650
+      "row": 36,
651
+      "schoolTarget": "254",
652
+      "schoolName": "上海赫贤学校",
653
+      "schoolTargetRemark": "",
654
+      "PlanNum": 2,
655
+      "noAdmission": true,
656
+      "officialRemark": "未录满",
657
+      "ScoreTotal": 0,
658
+      "Score1": 0,
659
+      "Score2": 0,
660
+      "Score3": 0,
661
+      "Score4": 0,
662
+      "ScoreTotalDifferenceValue": -523,
663
+      "previousScoreTotal": 523
664
+    },
665
+    {
666
+      "id": 39636,
667
+      "page": 1,
668
+      "row": 37,
669
+      "schoolTarget": "995",
670
+      "schoolName": "上海松江区爱菊学校",
671
+      "schoolTargetRemark": "",
672
+      "PlanNum": 5,
673
+      "noAdmission": false,
674
+      "officialRemark": "未录满",
675
+      "ScoreTotal": 506,
676
+      "Score1": 303.5,
677
+      "Score2": 80,
678
+      "Score3": 110,
679
+      "Score4": 78,
680
+      "ScoreTotalDifferenceValue": -24,
681
+      "previousScoreTotal": 530
682
+    },
683
+    {
684
+      "id": 39595,
685
+      "page": 1,
686
+      "row": 38,
687
+      "schoolTarget": "1036",
688
+      "schoolName": "上海师范大学附属青浦实验中学",
689
+      "schoolTargetRemark": "",
690
+      "PlanNum": 176,
691
+      "noAdmission": false,
692
+      "officialRemark": "",
693
+      "ScoreTotal": 626.5,
694
+      "Score1": 360.5,
695
+      "Score2": 119,
696
+      "Score3": 125,
697
+      "Score4": 127,
698
+      "ScoreTotalDifferenceValue": null,
699
+      "previousScoreTotal": null
700
+    },
701
+    {
702
+      "id": 39592,
703
+      "page": 1,
704
+      "row": 39,
705
+      "schoolTarget": "67",
706
+      "schoolName": "上海市青浦高级中学",
707
+      "schoolTargetRemark": "",
708
+      "PlanNum": 164,
709
+      "noAdmission": false,
710
+      "officialRemark": "",
711
+      "ScoreTotal": 671.5,
712
+      "Score1": 395,
713
+      "Score2": 133,
714
+      "Score3": 118,
715
+      "Score4": 131,
716
+      "ScoreTotalDifferenceValue": 3,
717
+      "previousScoreTotal": 668.5
718
+    },
719
+    {
720
+      "id": 39591,
721
+      "page": 1,
722
+      "row": 40,
723
+      "schoolTarget": "69",
724
+      "schoolName": "复旦大学附属中学青浦分校",
725
+      "schoolTargetRemark": "",
726
+      "PlanNum": 63,
727
+      "noAdmission": false,
728
+      "officialRemark": "",
729
+      "ScoreTotal": 695,
730
+      "Score1": 412,
731
+      "Score2": 137,
732
+      "Score3": 129,
733
+      "Score4": 138,
734
+      "ScoreTotalDifferenceValue": 5.5,
735
+      "previousScoreTotal": 689.5
736
+    },
737
+    {
738
+      "id": 39593,
739
+      "page": 1,
740
+      "row": 41,
741
+      "schoolTarget": "68",
742
+      "schoolName": "上海市朱家角中学",
743
+      "schoolTargetRemark": "",
744
+      "PlanNum": 160,
745
+      "noAdmission": false,
746
+      "officialRemark": "",
747
+      "ScoreTotal": 655.5,
748
+      "Score1": 379.5,
749
+      "Score2": 117,
750
+      "Score3": 119,
751
+      "Score4": 133,
752
+      "ScoreTotalDifferenceValue": -0.5,
753
+      "previousScoreTotal": 656
754
+    },
755
+    {
756
+      "id": 39597,
757
+      "page": 1,
758
+      "row": 42,
759
+      "schoolTarget": "257",
760
+      "schoolName": "上海市青浦区第二中学",
761
+      "schoolTargetRemark": "",
762
+      "PlanNum": 352,
763
+      "noAdmission": false,
764
+      "officialRemark": "",
765
+      "ScoreTotal": 555.5,
766
+      "Score1": 322.5,
767
+      "Score2": 93,
768
+      "Score3": 106,
769
+      "Score4": 100,
770
+      "ScoreTotalDifferenceValue": -15,
771
+      "previousScoreTotal": 570.5
772
+    },
773
+    {
774
+      "id": 39596,
775
+      "page": 1,
776
+      "row": 43,
777
+      "schoolTarget": "258",
778
+      "schoolName": "上海市青浦区东湖中学",
779
+      "schoolTargetRemark": "",
780
+      "PlanNum": 460,
781
+      "noAdmission": false,
782
+      "officialRemark": "",
783
+      "ScoreTotal": 599,
784
+      "Score1": 365,
785
+      "Score2": 112,
786
+      "Score3": 123,
787
+      "Score4": 92,
788
+      "ScoreTotalDifferenceValue": -4,
789
+      "previousScoreTotal": 603
790
+    },
791
+    {
792
+      "id": 39594,
793
+      "page": 1,
794
+      "row": 44,
795
+      "schoolTarget": "256",
796
+      "schoolName": "上海市青浦区第一中学",
797
+      "schoolTargetRemark": "",
798
+      "PlanNum": 371,
799
+      "noAdmission": false,
800
+      "officialRemark": "",
801
+      "ScoreTotal": 632,
802
+      "Score1": 377,
803
+      "Score2": 125,
804
+      "Score3": 120,
805
+      "Score4": 117,
806
+      "ScoreTotalDifferenceValue": 1,
807
+      "previousScoreTotal": 631
808
+    },
809
+    {
810
+      "id": 39598,
811
+      "page": 1,
812
+      "row": 45,
813
+      "schoolTarget": "863",
814
+      "schoolName": "上海青浦区世外高级中学",
815
+      "schoolTargetRemark": "",
816
+      "PlanNum": 27,
817
+      "noAdmission": false,
818
+      "officialRemark": "未录满",
819
+      "ScoreTotal": 545,
820
+      "Score1": 294,
821
+      "Score2": 90,
822
+      "Score3": 113,
823
+      "Score4": 110,
824
+      "ScoreTotalDifferenceValue": 26.5,
825
+      "previousScoreTotal": 518.5
826
+    },
827
+    {
828
+      "id": 39599,
829
+      "page": 1,
830
+      "row": 46,
831
+      "schoolTarget": "862",
832
+      "schoolName": "上海青浦区宏润博源高级中学",
833
+      "schoolTargetRemark": "",
834
+      "PlanNum": 37,
835
+      "noAdmission": false,
836
+      "officialRemark": "未录满",
837
+      "ScoreTotal": 493.5,
838
+      "Score1": 280.5,
839
+      "Score2": 88,
840
+      "Score3": 105,
841
+      "Score4": 86,
842
+      "ScoreTotalDifferenceValue": -64,
843
+      "previousScoreTotal": 557.5
844
+    },
845
+    {
846
+      "id": 39600,
847
+      "page": 1,
848
+      "row": 47,
849
+      "schoolTarget": "259",
850
+      "schoolName": "上海宋庆龄学校",
851
+      "schoolTargetRemark": "",
852
+      "PlanNum": 3,
853
+      "noAdmission": false,
854
+      "officialRemark": "",
855
+      "ScoreTotal": 614.5,
856
+      "Score1": 358.5,
857
+      "Score2": 101,
858
+      "Score3": 112,
859
+      "Score4": 118,
860
+      "ScoreTotalDifferenceValue": 21.5,
861
+      "previousScoreTotal": 593
862
+    },
863
+    {
864
+      "id": 39601,
865
+      "page": 1,
866
+      "row": 48,
867
+      "schoolTarget": "260",
868
+      "schoolName": "上海青浦区协和双语学校",
869
+      "schoolTargetRemark": "",
870
+      "PlanNum": 28,
871
+      "noAdmission": false,
872
+      "officialRemark": "未录满",
873
+      "ScoreTotal": 530.5,
874
+      "Score1": 304.5,
875
+      "Score2": 95,
876
+      "Score3": 102,
877
+      "Score4": 94,
878
+      "ScoreTotalDifferenceValue": -6,
879
+      "previousScoreTotal": 536.5
880
+    },
881
+    {
882
+      "id": 39639,
883
+      "page": 1,
884
+      "row": 49,
885
+      "schoolTarget": "267",
886
+      "schoolName": "上海美达菲双语高级中学",
887
+      "schoolTargetRemark": "",
888
+      "PlanNum": 3,
889
+      "noAdmission": false,
890
+      "officialRemark": "未录满",
891
+      "ScoreTotal": 529.5,
892
+      "Score1": 295.5,
893
+      "Score2": 83,
894
+      "Score3": 113,
895
+      "Score4": 102,
896
+      "ScoreTotalDifferenceValue": null,
897
+      "previousScoreTotal": null
898
+    },
899
+    {
900
+      "id": 39640,
901
+      "page": 1,
902
+      "row": 50,
903
+      "schoolTarget": "266",
904
+      "schoolName": "上海奉贤区博华高级中学",
905
+      "schoolTargetRemark": "",
906
+      "PlanNum": 6,
907
+      "noAdmission": false,
908
+      "officialRemark": "",
909
+      "ScoreTotal": 507.5,
910
+      "Score1": 277.5,
911
+      "Score2": 93,
912
+      "Score3": 91,
913
+      "Score4": 99,
914
+      "ScoreTotalDifferenceValue": -45,
915
+      "previousScoreTotal": 552.5
916
+    },
917
+    {
918
+      "id": 39641,
919
+      "page": 1,
920
+      "row": 51,
921
+      "schoolTarget": "268",
922
+      "schoolName": "上海市崇明区城桥中学",
923
+      "schoolTargetRemark": "",
924
+      "PlanNum": 1,
925
+      "noAdmission": false,
926
+      "officialRemark": "",
927
+      "ScoreTotal": 574.5,
928
+      "Score1": 325.5,
929
+      "Score2": 100,
930
+      "Score3": 114,
931
+      "Score4": 110,
932
+      "ScoreTotalDifferenceValue": 42,
933
+      "previousScoreTotal": 532.5
934
+    },
935
+    {
936
+      "id": 39642,
937
+      "page": 1,
938
+      "row": 52,
939
+      "schoolTarget": "271",
940
+      "schoolName": "上海市崇明区堡镇中学",
941
+      "schoolTargetRemark": "",
942
+      "PlanNum": 2,
943
+      "noAdmission": false,
944
+      "officialRemark": "",
945
+      "ScoreTotal": 579,
946
+      "Score1": 323,
947
+      "Score2": 98,
948
+      "Score3": 119,
949
+      "Score4": 116,
950
+      "ScoreTotalDifferenceValue": null,
951
+      "previousScoreTotal": null
952
+    },
953
+    {
954
+      "id": 39643,
955
+      "page": 1,
956
+      "row": 53,
957
+      "schoolTarget": "272",
958
+      "schoolName": "上海民办民一中学",
959
+      "schoolTargetRemark": "",
960
+      "PlanNum": 2,
961
+      "noAdmission": false,
962
+      "officialRemark": "",
963
+      "ScoreTotal": 451.5,
964
+      "Score1": 227.5,
965
+      "Score2": 105,
966
+      "Score3": 88,
967
+      "Score4": 95,
968
+      "ScoreTotalDifferenceValue": -48,
969
+      "previousScoreTotal": 499.5
970
+    },
971
+    {
972
+      "id": 39644,
973
+      "page": 1,
974
+      "row": 54,
975
+      "schoolTarget": "1039",
976
+      "schoolName": "上海新纪元双语学校",
977
+      "schoolTargetRemark": "",
978
+      "PlanNum": 15,
979
+      "noAdmission": true,
980
+      "officialRemark": "未录满",
981
+      "ScoreTotal": 0,
982
+      "Score1": 0,
983
+      "Score2": 0,
984
+      "Score3": 0,
985
+      "Score4": 0,
986
+      "ScoreTotalDifferenceValue": null,
987
+      "previousScoreTotal": null
988
+    }
989
+  ],
990
+  "not_in_official_image": []
991
+}

Разлика између датотеке није приказан због своје велике величине
+ 9317 - 0
秒过分数线数据导入/mps_score_1_15_verified_generic_sources_2026.json


Разлика између датотеке није приказан због своје велике величине
+ 1242 - 0
秒过分数线数据导入/mps_score_1_15_xuhui_scores_2026_import_report.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 4592
秒过分数线数据导入/mps_score_huangpu_scores_2026_backup.json


+ 0 - 104
秒过分数线数据导入/mps_score_qingpu_yizhong_shangmei_2026_backup.json

@@ -1,104 +0,0 @@
1
-[
2
-  {
3
-    "ID": 39138,
4
-    "ScoreYear": "2026",
5
-    "ScoreType": "名额到校",
6
-    "DistrictID": 14,
7
-    "SchoolOfGraduation": 752,
8
-    "SchoolFullNameJunior": "上海市青浦区尚美中学",
9
-    "SchoolTarget": "69",
10
-    "SchoolFullName": "复旦大学附属中学青浦分校",
11
-    "PlanNum": 3,
12
-    "ScoreTotal": "0.00",
13
-    "Score1": "0.00",
14
-    "Score2": "0.00",
15
-    "Score3": "0.00",
16
-    "Score4": "0.00",
17
-    "ScoreTotalDifferenceValue": "0.00"
18
-  },
19
-  {
20
-    "ID": 39153,
21
-    "ScoreYear": "2026",
22
-    "ScoreType": "名额到校",
23
-    "DistrictID": 14,
24
-    "SchoolOfGraduation": 766,
25
-    "SchoolFullNameJunior": "上海市青浦区第一中学",
26
-    "SchoolTarget": "69",
27
-    "SchoolFullName": "复旦大学附属中学青浦分校",
28
-    "PlanNum": 9,
29
-    "ScoreTotal": "0.00",
30
-    "Score1": "0.00",
31
-    "Score2": "0.00",
32
-    "Score3": "0.00",
33
-    "Score4": "0.00",
34
-    "ScoreTotalDifferenceValue": "0.00"
35
-  },
36
-  {
37
-    "ID": 39169,
38
-    "ScoreYear": "2026",
39
-    "ScoreType": "名额到校",
40
-    "DistrictID": 14,
41
-    "SchoolOfGraduation": 752,
42
-    "SchoolFullNameJunior": "上海市青浦区尚美中学",
43
-    "SchoolTarget": "67",
44
-    "SchoolFullName": "上海市青浦高级中学",
45
-    "PlanNum": 6,
46
-    "ScoreTotal": "0.00",
47
-    "Score1": "0.00",
48
-    "Score2": "0.00",
49
-    "Score3": "0.00",
50
-    "Score4": "0.00",
51
-    "ScoreTotalDifferenceValue": "0.00"
52
-  },
53
-  {
54
-    "ID": 39184,
55
-    "ScoreYear": "2026",
56
-    "ScoreType": "名额到校",
57
-    "DistrictID": 14,
58
-    "SchoolOfGraduation": 766,
59
-    "SchoolFullNameJunior": "上海市青浦区第一中学",
60
-    "SchoolTarget": "67",
61
-    "SchoolFullName": "上海市青浦高级中学",
62
-    "PlanNum": 20,
63
-    "ScoreTotal": "0.00",
64
-    "Score1": "0.00",
65
-    "Score2": "0.00",
66
-    "Score3": "0.00",
67
-    "Score4": "0.00",
68
-    "ScoreTotalDifferenceValue": "0.00"
69
-  },
70
-  {
71
-    "ID": 39200,
72
-    "ScoreYear": "2026",
73
-    "ScoreType": "名额到校",
74
-    "DistrictID": 14,
75
-    "SchoolOfGraduation": 752,
76
-    "SchoolFullNameJunior": "上海市青浦区尚美中学",
77
-    "SchoolTarget": "68",
78
-    "SchoolFullName": "上海市朱家角中学",
79
-    "PlanNum": 6,
80
-    "ScoreTotal": "0.00",
81
-    "Score1": "0.00",
82
-    "Score2": "0.00",
83
-    "Score3": "0.00",
84
-    "Score4": "0.00",
85
-    "ScoreTotalDifferenceValue": "0.00"
86
-  },
87
-  {
88
-    "ID": 39215,
89
-    "ScoreYear": "2026",
90
-    "ScoreType": "名额到校",
91
-    "DistrictID": 14,
92
-    "SchoolOfGraduation": 766,
93
-    "SchoolFullNameJunior": "上海市青浦区第一中学",
94
-    "SchoolTarget": "68",
95
-    "SchoolFullName": "上海市朱家角中学",
96
-    "PlanNum": 19,
97
-    "ScoreTotal": "0.00",
98
-    "Score1": "0.00",
99
-    "Score2": "0.00",
100
-    "Score3": "0.00",
101
-    "Score4": "0.00",
102
-    "ScoreTotalDifferenceValue": "0.00"
103
-  }
104
-]

Разлика између датотеке није приказан због своје велике величине
+ 0 - 52814
秒过分数线数据导入/mps_score_school_quota_2026_bad_targets_backup.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 2361
秒过分数线数据导入/mps_score_school_quota_2026_qingpu_cross_page_fix_backup.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 2360
秒过分数线数据导入/mps_score_school_quota_2026_qingpu_reparse_backup.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 78746
秒过分数线数据导入/mps_score_scores_2026_backup.json


+ 0 - 104
秒过分数线数据导入/mps_score_scores_2026_tie_preferred_backup.json

@@ -1,104 +0,0 @@
1
-[
2
-  {
3
-    "ID": 32299,
4
-    "ScoreYear": "2026",
5
-    "ScoreType": "名额到区",
6
-    "DistrictID": 5,
7
-    "SchoolOfGraduation": 0,
8
-    "SchoolFullNameJunior": null,
9
-    "SchoolTarget": "55",
10
-    "SchoolFullName": "上海市进才中学",
11
-    "PlanNum": 5,
12
-    "ScoreTotal": "700.50",
13
-    "Score1": "0.00",
14
-    "Score2": "0.00",
15
-    "Score3": "0.00",
16
-    "Score4": "0.00",
17
-    "ScoreTotalDifferenceValue": "0.00"
18
-  },
19
-  {
20
-    "ID": 32318,
21
-    "ScoreYear": "2026",
22
-    "ScoreType": "名额到区",
23
-    "DistrictID": 7,
24
-    "SchoolOfGraduation": 0,
25
-    "SchoolFullNameJunior": null,
26
-    "SchoolTarget": "2",
27
-    "SchoolFullName": "上海交通大学附属中学",
28
-    "PlanNum": 47,
29
-    "ScoreTotal": "0.00",
30
-    "Score1": "0.00",
31
-    "Score2": "0.00",
32
-    "Score3": "0.00",
33
-    "Score4": "0.00",
34
-    "ScoreTotalDifferenceValue": "0.00"
35
-  },
36
-  {
37
-    "ID": 33669,
38
-    "ScoreYear": "2026",
39
-    "ScoreType": "名额到校",
40
-    "DistrictID": 7,
41
-    "SchoolOfGraduation": 425,
42
-    "SchoolFullNameJunior": "上海市铁岭中学",
43
-    "SchoolTarget": "40",
44
-    "SchoolFullName": "上海市控江中学",
45
-    "PlanNum": 14,
46
-    "ScoreTotal": "0.00",
47
-    "Score1": "0.00",
48
-    "Score2": "0.00",
49
-    "Score3": "0.00",
50
-    "Score4": "0.00",
51
-    "ScoreTotalDifferenceValue": "0.00"
52
-  },
53
-  {
54
-    "ID": 34049,
55
-    "ScoreYear": "2026",
56
-    "ScoreType": "名额到校",
57
-    "DistrictID": 8,
58
-    "SchoolOfGraduation": 487,
59
-    "SchoolFullNameJunior": "上海市七宝中学附属鑫都实验中学",
60
-    "SchoolTarget": "44",
61
-    "SchoolFullName": "华东师范大学第二附属中学闵行紫竹分校",
62
-    "PlanNum": 7,
63
-    "ScoreTotal": "0.00",
64
-    "Score1": "0.00",
65
-    "Score2": "0.00",
66
-    "Score3": "0.00",
67
-    "Score4": "0.00",
68
-    "ScoreTotalDifferenceValue": "0.00"
69
-  },
70
-  {
71
-    "ID": 37011,
72
-    "ScoreYear": "2026",
73
-    "ScoreType": "名额到校",
74
-    "DistrictID": 9,
75
-    "SchoolOfGraduation": 500,
76
-    "SchoolFullNameJunior": "上海市淞谊中学",
77
-    "SchoolTarget": "49",
78
-    "SchoolFullName": "上海市吴淞中学",
79
-    "PlanNum": 7,
80
-    "ScoreTotal": "0.00",
81
-    "Score1": "0.00",
82
-    "Score2": "0.00",
83
-    "Score3": "0.00",
84
-    "Score4": "0.00",
85
-    "ScoreTotalDifferenceValue": "0.00"
86
-  },
87
-  {
88
-    "ID": 38643,
89
-    "ScoreYear": "2026",
90
-    "ScoreType": "名额到校",
91
-    "DistrictID": 13,
92
-    "SchoolOfGraduation": 1019,
93
-    "SchoolFullNameJunior": "上海外国语大学附属外国语学校松江云间中学",
94
-    "SchoolTarget": "63",
95
-    "SchoolFullName": "上海市松江二中",
96
-    "PlanNum": 6,
97
-    "ScoreTotal": "0.00",
98
-    "Score1": "0.00",
99
-    "Score2": "0.00",
100
-    "Score3": "0.00",
101
-    "Score4": "0.00",
102
-    "ScoreTotalDifferenceValue": "0.00"
103
-  }
104
-]

+ 54 - 0
秒过分数线数据导入/ocr_image_text.swift

@@ -0,0 +1,54 @@
1
+import Foundation
2
+import Vision
3
+import AppKit
4
+
5
+struct OcrLine: Encodable {
6
+  let text: String
7
+  let x: Double
8
+  let y: Double
9
+  let width: Double
10
+  let height: Double
11
+}
12
+
13
+let args = CommandLine.arguments
14
+guard args.count >= 2 else {
15
+  fputs("usage: swift ocr_image_text.swift <image>\\n", stderr)
16
+  exit(2)
17
+}
18
+
19
+let imageURL = URL(fileURLWithPath: args[1])
20
+guard let image = NSImage(contentsOf: imageURL),
21
+      let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
22
+  fputs("failed to load image: \\(args[1])\\n", stderr)
23
+  exit(1)
24
+}
25
+
26
+let request = VNRecognizeTextRequest()
27
+request.recognitionLevel = .accurate
28
+request.usesLanguageCorrection = true
29
+request.recognitionLanguages = ["zh-Hans", "en-US"]
30
+request.minimumTextHeight = 0.006
31
+
32
+let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
33
+try handler.perform([request])
34
+
35
+let observations = request.results ?? []
36
+let lines = observations.compactMap { observation -> OcrLine? in
37
+  guard let candidate = observation.topCandidates(1).first else { return nil }
38
+  let box = observation.boundingBox
39
+  return OcrLine(
40
+    text: candidate.string,
41
+    x: Double(box.minX),
42
+    y: Double(box.minY),
43
+    width: Double(box.width),
44
+    height: Double(box.height)
45
+  )
46
+}.sorted {
47
+  let y0 = Int($0.y * 1000)
48
+  let y1 = Int($1.y * 1000)
49
+  if y0 != y1 { return y0 > y1 }
50
+  return $0.x < $1.x
51
+}
52
+
53
+let data = try JSONEncoder().encode(lines)
54
+FileHandle.standardOutput.write(data)