from pathlib import Path from docx import Document from docx.enum.section import WD_SECTION_START from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Cm, Inches, Pt, RGBColor ROOT = Path(r"D:\Look at the mountain") OUT = ROOT / "志愿通数据审核系统使用手册.docx" SHOT = ROOT / "tmp" / "review-manual-shots" BLUE = "1F4E78" MID_BLUE = "5B9BD5" LIGHT_BLUE = "D9EAF7" LIGHTER_BLUE = "EDF4FA" GRAY = "666666" LIGHT_GRAY = "F2F2F2" BORDER = "D9D9D9" WHITE = "FFFFFF" BLACK = "111111" GREEN = "70AD47" YELLOW = "FFC000" RED = "C00000" def set_cell_shading(cell, fill): tc_pr = cell._tc.get_or_add_tcPr() shd = tc_pr.find(qn("w:shd")) if shd is None: shd = OxmlElement("w:shd") tc_pr.append(shd) shd.set(qn("w:fill"), fill) def set_cell_border(cell, color=BORDER, size="6"): tc_pr = cell._tc.get_or_add_tcPr() borders = tc_pr.first_child_found_in("w:tcBorders") if borders is None: borders = OxmlElement("w:tcBorders") tc_pr.append(borders) for edge in ("top", "left", "bottom", "right", "insideH", "insideV"): tag = "w:" + edge el = borders.find(qn(tag)) if el is None: el = OxmlElement(tag) borders.append(el) el.set(qn("w:val"), "single") el.set(qn("w:sz"), size) el.set(qn("w:space"), "0") el.set(qn("w:color"), color) def set_cell_margins(cell, top=100, start=120, bottom=100, end=120): tc = cell._tc tc_pr = tc.get_or_add_tcPr() tc_mar = tc_pr.first_child_found_in("w:tcMar") if tc_mar is None: tc_mar = OxmlElement("w:tcMar") tc_pr.append(tc_mar) for m, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)): node = tc_mar.find(qn("w:" + m)) if node is None: node = OxmlElement("w:" + m) tc_mar.append(node) node.set(qn("w:w"), str(value)) node.set(qn("w:type"), "dxa") def prevent_row_split(row): tr_pr = row._tr.get_or_add_trPr() cant_split = OxmlElement("w:cantSplit") tr_pr.append(cant_split) def repeat_table_header(row): tr_pr = row._tr.get_or_add_trPr() tbl_header = OxmlElement("w:tblHeader") tbl_header.set(qn("w:val"), "true") tr_pr.append(tbl_header) def set_repeat_header_text(run, color=WHITE, bold=True): run.bold = bold run.font.color.rgb = RGBColor.from_string(color) run.font.size = Pt(9) run.font.name = "微软雅黑" run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑") def add_page_number(paragraph): paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER run = paragraph.add_run("志愿通数据审核系统使用手册 · 第 ") run.font.size = Pt(8.5) run.font.color.rgb = RGBColor.from_string(GRAY) begin = OxmlElement("w:fldChar") begin.set(qn("w:fldCharType"), "begin") instr = OxmlElement("w:instrText") instr.set(qn("xml:space"), "preserve") instr.text = " PAGE " separate = OxmlElement("w:fldChar") separate.set(qn("w:fldCharType"), "separate") text = OxmlElement("w:t") text.text = "1" end = OxmlElement("w:fldChar") end.set(qn("w:fldCharType"), "end") r = OxmlElement("w:r") r_pr = OxmlElement("w:rPr") color = OxmlElement("w:color") color.set(qn("w:val"), GRAY) size = OxmlElement("w:sz") size.set(qn("w:val"), "17") r_pr.append(color) r_pr.append(size) r.append(r_pr) r.extend([begin, instr, separate, text, end]) paragraph._p.append(r) tail = paragraph.add_run(" 页") tail.font.size = Pt(8.5) tail.font.color.rgb = RGBColor.from_string(GRAY) def set_run_font(run, name="微软雅黑", size=10.5, color=BLACK, bold=None): run.font.name = name run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name) run.font.size = Pt(size) run.font.color.rgb = RGBColor.from_string(color) if bold is not None: run.bold = bold def add_para(doc, text="", *, style=None, bold_lead=None, keep=False, space_after=4, align=None): p = doc.add_paragraph(style=style) p.paragraph_format.space_after = Pt(space_after) p.paragraph_format.line_spacing = 1.25 if keep: p.paragraph_format.keep_with_next = True if align is not None: p.alignment = align if bold_lead and text.startswith(bold_lead): r1 = p.add_run(bold_lead) set_run_font(r1, bold=True) r2 = p.add_run(text[len(bold_lead):]) set_run_font(r2) else: r = p.add_run(text) set_run_font(r) return p def add_bullet(doc, text, level=0): p = doc.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2") p.paragraph_format.left_indent = Cm(0.63 + level * 0.5) p.paragraph_format.first_line_indent = Cm(-0.25) p.paragraph_format.space_after = Pt(2) p.paragraph_format.line_spacing = 1.18 r = p.add_run(text) set_run_font(r, size=10) return p _step_counter = 0 def add_number(doc, text): global _step_counter _step_counter += 1 p = doc.add_paragraph() p.paragraph_format.left_indent = Cm(0.65) p.paragraph_format.first_line_indent = Cm(-0.28) p.paragraph_format.space_after = Pt(3) p.paragraph_format.line_spacing = 1.2 n = p.add_run(f"{_step_counter}. ") set_run_font(n, size=10, color=BLUE, bold=True) r = p.add_run(text) set_run_font(r, size=10) return p def add_heading(doc, text, level=1): global _step_counter if level <= 2: _step_counter = 0 p = doc.add_paragraph(style=f"Heading {level}") p.paragraph_format.keep_with_next = True p.paragraph_format.space_before = Pt(12 if level == 1 else 8) p.paragraph_format.space_after = Pt(5) r = p.add_run(text) set_run_font(r, size=17 if level == 1 else 13 if level == 2 else 11.5, color=BLUE if level <= 2 else BLACK, bold=True) return p def add_note(doc, lead, text): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(3) p.paragraph_format.space_after = Pt(5) p.paragraph_format.line_spacing = 1.22 r1 = p.add_run(lead) set_run_font(r1, size=10, color=BLUE, bold=True) r2 = p.add_run(text) set_run_font(r2, size=10) return p def add_table(doc, headers, rows, widths=None, font_size=8.8): table = doc.add_table(rows=1, cols=len(headers)) table.alignment = WD_TABLE_ALIGNMENT.CENTER table.autofit = False hdr = table.rows[0] repeat_table_header(hdr) for i, header in enumerate(headers): cell = hdr.cells[i] if widths: cell.width = Cm(widths[i]) set_cell_shading(cell, BLUE) set_cell_border(cell) set_cell_margins(cell) cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER p = cell.paragraphs[0] p.paragraph_format.space_after = Pt(0) run = p.add_run(str(header)) set_repeat_header_text(run) for ridx, row_data in enumerate(rows): row = table.add_row() prevent_row_split(row) if ridx % 2 == 1: for cell in row.cells: set_cell_shading(cell, LIGHT_GRAY) for i, value in enumerate(row_data): cell = row.cells[i] if widths: cell.width = Cm(widths[i]) set_cell_border(cell) set_cell_margins(cell) cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER p = cell.paragraphs[0] p.paragraph_format.space_after = Pt(0) p.paragraph_format.line_spacing = 1.08 run = p.add_run(str(value)) set_run_font(run, size=font_size) doc.add_paragraph().paragraph_format.space_after = Pt(0) return table def add_figure(doc, filename, caption, width=6.75): path = SHOT / filename if not path.exists(): raise FileNotFoundError(path) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.keep_with_next = True p.paragraph_format.space_before = Pt(4) p.paragraph_format.space_after = Pt(3) p.add_run().add_picture(str(path), width=Inches(width)) cap = doc.add_paragraph() cap.alignment = WD_ALIGN_PARAGRAPH.CENTER cap.paragraph_format.space_after = Pt(7) r = cap.add_run(caption) set_run_font(r, size=8.5, color=GRAY) def add_page_break(doc): doc.add_page_break() doc = Document() section = doc.sections[0] section.page_width = Inches(8.5) section.page_height = Inches(11) section.top_margin = Inches(0.68) section.bottom_margin = Inches(0.63) section.left_margin = Inches(0.72) section.right_margin = Inches(0.72) section.header_distance = Inches(0.28) section.footer_distance = Inches(0.28) styles = doc.styles normal = styles["Normal"] normal.font.name = "微软雅黑" normal._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑") normal.font.size = Pt(10.5) normal.font.color.rgb = RGBColor.from_string(BLACK) normal.paragraph_format.space_after = Pt(4) normal.paragraph_format.line_spacing = 1.25 for sname in ("Title", "Subtitle"): styles[sname].font.name = "微软雅黑" styles[sname]._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑") for i in range(1, 4): style = styles[f"Heading {i}"] style.font.name = "微软雅黑" style._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑") style.font.color.rgb = RGBColor.from_string(BLUE if i <= 2 else BLACK) # Header/footer hp = section.header.paragraphs[0] hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT hr = hp.add_run("志愿通 · 数据审核系统") set_run_font(hr, size=8.5, color=GRAY) add_page_number(section.footer.paragraphs[0]) # Cover for _ in range(4): doc.add_paragraph() p = doc.add_paragraph(style="Title") p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(18) r = p.add_run("志愿通数据审核系统\n使用手册") set_run_font(r, size=30, color=BLUE, bold=True) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(28) r = p.add_run("数据查看 · 来源追溯 · 逐条审核 · 覆盖管理") set_run_font(r, size=13, color=GRAY) meta = add_table(doc, ["文档信息", "内容"], [ ["文档版本", "V1.0"], ["适用系统", "志愿通数据审核系统(本地审核环境)"], ["适用角色", "数据采集、数据审核、产品运营、项目管理人员"], ["编制日期", "2026年9月6日"], ], widths=[3.0, 12.5], font_size=10) doc.add_paragraph() p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(18) r = p.add_run("内部使用文档") set_run_font(r, size=10, color=GRAY, bold=True) add_page_break(doc) # Contents and introduction add_heading(doc, "目录", 1) add_para(doc, "本手册按实际使用顺序组织,既可从头阅读,也可直接定位到具体数据集。") add_table(doc, ["章节", "主要内容"], [ ["1. 文档说明", "使用范围、角色、基本原则"], ["2. 快速开始", "访问系统、完成一次审核的最短路径"], ["3. 首页与导航", "数据总览、左侧菜单、指标含义"], ["4. 省份与年份", "采集矩阵、灰色状态、范围页"], ["5. 通用查询与详情", "筛选、分页、详情抽屉、来源追溯"], ["6. 审核操作", "状态选择、审核人、备注、审计日志"], ["7. 数据集审核要点", "院校、计划、分数、覆盖、来源等分类规则"], ["8. 推荐审核流程", "按依赖关系组织审核批次"], ["9. 覆盖与来源管理", "覆盖口径、来源类型、网页与哈希"], ["10. 常见场景", "按省年审核、定位异常、核对分数"], ["11. 故障排查与常见问题", "连接、空列表、保存失败、性能"], ["附录", "状态、字段和审核记录用语"], ], widths=[4.3, 11.2], font_size=9.2) add_heading(doc, "1. 文档说明", 1) add_heading(doc, "1.1 使用范围", 2) add_para(doc, "本系统用于查看高校基础库及各省高考招录数据,核对来源和字段,并保存审核结论。它覆盖全国院校、批次与段次、计划发布批次、院校专业组、专业招生计划、专业录取线、一分一段、省级控制线、院校投档线、覆盖与缺口、来源档案等数据。") add_heading(doc, "1.2 角色与职责", 2) add_table(doc, ["角色", "主要职责", "建议权限边界"], [ ["采集人员", "登记来源、导入数据、补充覆盖说明", "不替代审核人员给出最终业务判断"], ["审核人员", "核对字段、来源、匹配关系并提交结论", "按可验证事实审核,疑点标记“需复核”"], ["产品/运营", "查看覆盖率、缺口和审核进度", "不直接改写原始采集事实"], ["项目管理员", "维护运行环境、账号和数据任务", "定期备份数据库与来源文件"], ], widths=[2.7, 7.1, 5.7], font_size=8.8) add_heading(doc, "1.3 三条基本原则", 2) add_bullet(doc, "来源优先:分数、位次、计划数等关键值必须能回溯到来源档案或官方页面。") add_bullet(doc, "不猜测缺失值:页面中的“—”、空值和“未标注”表示源数据未提供或当前未解析,不应凭经验补写。") add_bullet(doc, "审核与原始数据分离:提交审核只更新审核状态并新增审计记录,不会改写原始采集数据。") add_heading(doc, "2. 快速开始", 1) add_heading(doc, "2.1 访问条件", 2) add_table(doc, ["项目", "默认值", "检查方法"], [ ["前端地址", "http://127.0.0.1:5173", "浏览器打开后应显示数据总览"], ["后端服务", "http://127.0.0.1:8080", "前端能显示统计卡片和列表即表示通信正常"], ["数据库", "MySQL 3306", "列表查询和审核保存均依赖数据库"], ["推荐浏览器", "Chrome / Edge 最新稳定版", "建议窗口宽度不低于 1280 像素"], ], widths=[3.1, 5.1, 7.3], font_size=9.2) add_note(doc, "提示:", "如果首页能打开但所有统计均为 0,通常是后端或数据库未连接,而不是系统真的没有数据。") add_heading(doc, "2.2 完成一次审核的最短路径", 2) for item in [ "进入“省份与年份”,选择目标年份和省份。绿色或蓝色通常表示已有数据;灰色表示尚未收集。", "进入范围页后,选择需要审核的数据集,例如“省级控制线”或“院校投档线”。", "使用关键词、科类、批次、分数范围和审核状态等条件缩小列表。", "点击行或“详情”,核对完整字段、来源标题、来源地址、稳定 ID 和历史审核记录。", "点击“审核”,选择结论,填写审核人;存在疑点时填写可复现的备注并保存。", ]: add_number(doc, item) add_heading(doc, "2.3 第一次使用建议", 2) add_bullet(doc, "先浏览“数据总览”和“覆盖与缺口”,了解当前采集范围,再开始逐条审核。") add_bullet(doc, "优先从一个省份、一个年份、一个数据集开始,避免审核上下文混淆。") add_bullet(doc, "首次提交时认真填写审核人姓名;系统会在当前浏览器中记住,后续仍可修改。") add_heading(doc, "3. 首页与导航", 1) add_figure(doc, "01-dashboard.png", "图 1 数据总览:首页集中显示数据规模、匹配质量与覆盖概况") add_heading(doc, "3.1 统计卡片怎么读", 2) add_para(doc, "首页统计卡片是当前数据库的即时汇总。截图时可见全国院校 2,988 所、院校专业组 200,185 条、专业招生计划 868,304 条、专业录取线 432,005 条、院校投档线 74,978 条、2025 一分一段 28,010 条、2025 省控线 257 条。数量会随重新导入、补采和数据治理而变化。") add_table(doc, ["指标", "用途", "重点关注"], [ ["院校/专业组/计划/分数线数量", "判断数据规模与导入结果", "异常突增、突减或长期为 0"], ["未匹配投档线", "发现无法关联到院校基础库的记录", "应优先完成学校代码和名称映射"], ["待审核业务记录", "评估审核工作量", "只表示当前审核状态,不等同于数据缺失"], ["匹配质量", "查看稳定 ID 关联情况", "未匹配项需追查代码、别名、年份口径"], ["最近覆盖状态", "快速查看省年数据覆盖", "部分覆盖和暂无公开全量需阅读备注"], ], widths=[4.0, 5.7, 5.8], font_size=8.8) add_heading(doc, "3.2 左侧导航", 2) add_table(doc, ["分组", "菜单", "作用"], [ ["工作台", "数据总览", "全局数据量、匹配质量、覆盖状态"], ["工作台", "省份与年份", "按 31 个省级地区和年份切换审核范围"], ["工作台", "审核工作台", "集中进入各类待审核业务数据"], ["院校基础库", "全国院校", "审核学校稳定 ID、教育层次、主管部门等"], ["各省招录", "批次/发布批次/专业组/计划/录取线/一分一段/省控线/投档线", "查看和审核各省招录业务数据"], ["数据治理", "覆盖与缺口", "记录公开数据可得性、采集范围和缺口"], ["数据治理", "来源档案", "核对来源发布方、标题、日期、网页和文件哈希"], ], widths=[2.5, 5.2, 7.8], font_size=8.6) add_heading(doc, "4. 省份与年份分类", 1) add_figure(doc, "02-scopes.png", "图 2 省份与年份矩阵:尚未收集的省份和年份以灰色显示") add_heading(doc, "4.1 状态颜色", 2) add_table(doc, ["显示状态", "常见颜色", "含义", "审核动作"], [ ["未收集", "灰色", "当前省份与年份尚无业务数据", "不要误判为查询错误;记录采集任务"], ["部分采集", "黄色", "已有部分数据,但品类、批次或覆盖范围不完整", "进入范围页阅读覆盖说明并确认缺口"], ["已采集", "绿色", "目标范围已有可审核数据", "开始逐数据集核验"], ["已核验", "蓝色", "该范围已完成既定核验", "必要时抽检来源与最新变更"], ], widths=[2.5, 2.5, 5.8, 4.7], font_size=8.7) add_note(doc, "特别说明:", "灰色只表示当前系统尚未收集,不代表该省份或年份没有公开数据;黄色也不代表数据错误,只说明覆盖仍不完整。") add_heading(doc, "4.2 使用步骤", 2) add_number(doc, "在页面顶部选择年份。系统当前可展示 2022—2026 等年度,具体可用数据以卡片颜色为准。") add_number(doc, "点击省份卡片进入该省、该年的范围页。后续打开的数据页会继承省份和年份上下文。") add_number(doc, "查看范围页中的各数据集数量卡片。数量为 0 的类别表示此省年当前没有该类记录。") add_number(doc, "从批次规则、专业组、计划、分数、覆盖和来源等入口进入具体列表。") add_heading(doc, "4.3 范围页的九类数据", 2) add_table(doc, ["类别", "主要回答的问题"], [ ["批次与段次", "本省当年如何划分批次、段次和志愿模式?"], ["计划发布批次", "哪一版招生计划、哪一轮次、何时发布?"], ["院校专业组", "院校代码、专业组代码和选科要求如何组织?"], ["专业招生计划", "每个专业计划招生多少人,有何选科与备注?"], ["专业录取线", "专业最低分、位次、最高分、平均分和录取人数是多少?"], ["一分一段", "某分数同分人数、累计人数和位次区间是多少?"], ["省级控制线", "不同科类和层次的文化/专业控制线是多少?"], ["院校投档线", "院校或专业组的投档分、位次和志愿序号是多少?"], ["覆盖与缺口", "公开数据是否完整、已采集多少、缺什么?"], ], widths=[4.0, 11.5], font_size=8.8) add_heading(doc, "5. 通用查询、列表与详情", 1) add_heading(doc, "5.1 筛选区", 2) add_para(doc, "不同数据页会按业务配置不同条件,但操作方式一致。输入关键词后可按 Enter 或点击“查询”;更改多个条件后再查询,可组合缩小范围。") add_table(doc, ["筛选类型", "示例", "使用建议"], [ ["省份/年份", "甘肃、2025", "先固定省年,避免跨口径比较"], ["关键词", "院校名、院校代码、专业名、组代码、发布方、哈希", "代码优先精确,名称可做模糊查找"], ["业务枚举", "科类、教育层次、批次、段次、所有制", "用于批量定位同一口径记录"], ["分数范围", "最低分至最高分", "排查极值、控制线附近或特定分段"], ["匹配状态", "已匹配/未匹配", "优先处理缺少稳定 ID 的记录"], ["审核状态", "待审核/已通过/需复核/已驳回", "复查时切换到特定结论"], ], widths=[3.0, 5.1, 7.4], font_size=8.8) add_bullet(doc, "重置:清空当前页的筛选条件并恢复默认范围。") add_bullet(doc, "刷新:重新从后端读取当前条件下的数据,适合审核保存或数据重新导入后使用。") add_bullet(doc, "分页:支持 20、50、100、200 条/页。大页适合扫查,小页更适合逐条审核。") add_heading(doc, "5.2 列表操作", 2) add_bullet(doc, "点击整行或“详情”打开详情抽屉;列表只展示高频字段,详情中可查看完整字段。") add_bullet(doc, "列表中的“稳定 ID”用于跨来源关联;缺失时应检查外部代码、院校别名或年份差异。") add_bullet(doc, "遇到长文本、备注或复合分值时,以详情中的完整值和来源页面为准。") add_heading(doc, "5.3 详情抽屉", 2) add_para(doc, "详情抽屉通常包含业务字段、内部稳定 ID、来源标题、来源地址、当前审核状态和审核历史。全国院校详情还可能展示外部代码映射、历史名称和校区。") add_note(doc, "核对顺序:", "先确认省份、年份、科类和批次上下文,再看院校/专业标识,然后核对分数、位次和计划数,最后打开来源页面确认。") add_heading(doc, "6. 审核操作", 1) add_heading(doc, "6.1 四种审核状态", 2) add_table(doc, ["状态", "何时使用", "备注建议"], [ ["待审核", "尚未形成审核结论,或需要撤回到待处理", "说明撤回原因或下一步"], ["已通过", "字段、口径、来源和关联关系均可验证", "可简写核对依据,关键记录建议写来源日期"], ["需复核", "存在疑点,但证据不足以判断为错误", "明确疑点字段、当前值、来源和建议复核动作"], ["已驳回", "确认存在错误、错源、错年、错省、重复或不可用记录", "写清错误事实和正确依据,便于后续修复"], ], widths=[2.5, 6.3, 6.7], font_size=8.7) add_heading(doc, "6.2 提交审核", 2) for item in [ "在列表点击“审核”,或先打开详情后点击“审核这条数据”。", "选择审核状态。没有充分证据时使用“需复核”,不要直接“已驳回”。", "填写审核人。该字段必填;系统会在当前浏览器中记住最近一次填写值。", "填写审核备注,最多 2,000 个字符。对关键数据和异常结论,建议记录可复现依据。", "点击保存。成功后当前状态被更新,同时新增一条带时间的审核日志。", "返回列表或刷新,确认状态和筛选结果已变化。", ]: add_number(doc, item) add_heading(doc, "6.3 审核记录机制", 2) add_para(doc, "系统将当前审核结果保存在审核记录中,并把每次操作追加到审计日志。详情页按时间倒序展示历史,便于判断一条记录经历过哪些结论。审核操作不会直接修改院校、计划、分数线、来源等原始业务字段。") add_heading(doc, "6.4 推荐备注格式", 2) add_table(doc, ["场景", "示例写法"], [ ["已通过", "已核对省教育考试院 2025-06-25 发布页面,科类、分数和位次一致。"], ["需复核", "最低分一致,但原网页未给出最低位次;请核对附件表第 3 页。"], ["已驳回", "院校代码属于 2024 年旧代码,本记录错误套用到 2025 年;应重新映射。"], ["覆盖说明", "当前仅采集普通类本科批,艺术类和征集志愿尚缺。"], ], widths=[3.2, 12.3], font_size=9) add_heading(doc, "7. 数据集审核要点", 1) add_heading(doc, "7.1 全国院校基础库", 2) add_table(doc, ["重点字段", "核验要点"], [ ["教育部院校代码", "应保持稳定并与学校主体一一对应;不能把招生代码当作教育部代码"], ["正式名称", "以主管部门或学校官网现行正式名称为准,历史名称进入别名"], ["省份/城市", "核对学校主体所在地;多校区可在校区信息中分别记录"], ["教育层次/所有制/主管部门", "以官方分类口径为准,避免按社会印象推断"], ["外部代码映射", "同一学校在不同省年可有不同招生代码,必须带来源和适用范围"], ], widths=[4.2, 11.3], font_size=8.8) add_heading(doc, "7.2 批次与段次、计划发布批次", 2) add_bullet(doc, "批次名称、段次、类别、教育层次、志愿模式、最大专业组志愿数、每组最大专业数和征集次数需按当年省级规则核对。") add_bullet(doc, "计划发布批次应记录发布标题、轮次、覆盖范围、发布日期和计划数;不同版本不得无依据覆盖。") add_bullet(doc, "同名批次在不同年份的规则可能变化,审核时不可沿用上一年度结论。") add_heading(doc, "7.3 院校专业组", 2) add_table(doc, ["字段", "审核重点"], [ ["院校代码/名称", "确认已映射到正确学校稳定 ID,处理简称、改名和历史代码"], ["专业组代码/名称", "核对前导零、字母大小写和当年编排规则"], ["组类型/选科要求", "区分普通组、专项、民族班等;选科组合不得自行标准化丢失信息"], ["批次/科类/段次", "必须与该省当年招生规则一致"], ], widths=[4.0, 11.5], font_size=8.8) add_heading(doc, "7.4 专业招生计划", 2) add_bullet(doc, "核对院校、专业组、专业代码、专业名称、选科要求、计划数和备注。") add_bullet(doc, "专业代码可能只在某省某年某批次内有效,不应直接作为全国稳定专业 ID。") add_bullet(doc, "计划数应为非负整数;合计校验要以官方发布口径为准,注意专项和征集计划是否另列。") add_heading(doc, "7.5 专业录取线", 2) add_table(doc, ["字段", "审核重点"], [ ["最低分/最低位次", "优先核对;位次必须使用相同科类和同分规则"], ["最高分/平均分", "源未公布时保留空值,不能从最低分推算"], ["录取人数", "与招生计划不必完全相等,需考虑扩招、退档、征集等"], ["院校/组/专业稳定 ID", "确保分数记录能关联到正确主体"], ["批次/段次/科类", "防止普通类、艺术类、专科、本科数据混入"], ], widths=[4.0, 11.5], font_size=8.8) add_heading(doc, "7.6 一分一段", 2) add_bullet(doc, "逐分核对分数、同分人数、累计人数、位次起点和位次终点。") add_bullet(doc, "累计人数通常应随分数下降而不减少;发现反向跳变、重复分值或缺档时标记“需复核”。") add_bullet(doc, "不同科类、赋分口径和综合分必须分开,不能用普通类位次解释艺术类综合分。") add_heading(doc, "7.7 省级控制线", 2) add_figure(doc, "04-control-lines.png", "图 3 省级控制线:可按科类、层次、分数范围和审核状态筛选") add_bullet(doc, "普通类重点核对科类、层次、文化分;艺术体育类还要核对专业分或综合分口径。") add_bullet(doc, "复合文本(如文化成绩与体检、专业要求并列)应完整保留到备注,不要强行拆成单一分数。") add_bullet(doc, "同一省份同一年可能有特殊类型招生控制线、资格线、段线等,应通过层次和备注区分。") add_heading(doc, "7.8 院校投档线", 2) add_table(doc, ["重点字段", "常见风险"], [ ["院校代码/名称", "招生代码变更、简称或合并导致错配"], ["专业组", "新高考省份把院校线误当专业组线"], ["投档分/最低位次", "分数口径不一致、位次来自错误科类"], ["志愿序号", "平行志愿与顺序志愿字段含义不同"], ["稳定 ID", "未匹配记录无法可靠关联院校基础库"], ["批次/段次/层次", "本科、专科、提前批、征集批混淆"], ], widths=[4.0, 11.5], font_size=8.8) add_heading(doc, "7.9 覆盖与缺口", 2) add_table(doc, ["覆盖状态", "使用口径"], [ ["公开完整快照", "按当前公开口径已获得完整可用快照,并能说明其范围"], ["公开部分数据", "仅覆盖部分批次、科类、学校、轮次或页面"], ["暂无公开全量", "未发现可公开获得的全量数据,可能仅有查询页或零散公告"], ], widths=[4.3, 11.2], font_size=9) add_bullet(doc, "记录数是当前导入量,不自动证明“完整”;完整性必须结合发布范围和缺口说明判断。") add_bullet(doc, "缺口备注应写清缺的是哪些批次、科类、轮次、文件或字段,而不是只写“数据不全”。") add_heading(doc, "7.10 来源档案", 2) add_bullet(doc, "核对来源类型、发布方、标题、发布日期、落地页、文件路径和内容哈希。") add_bullet(doc, "同一文件的哈希可用于发现重复导入或内容变更;网页标题相同不代表内容完全一致。") add_bullet(doc, "优先官方来源;转载页应标注转载发布方和原始来源,并谨慎用于最终结论。") add_heading(doc, "8. 推荐审核流程", 1) add_figure(doc, "03-review.png", "图 4 审核工作台:按数据依赖关系组织审核入口") add_heading(doc, "8.1 为什么要按顺序审核", 2) add_para(doc, "招录数据之间存在关联:院校基础库和代码映射不可靠,会继续影响专业组、计划、投档线和专业录取线。建议先处理主体与匹配,再处理数量和分数,最后核对覆盖与来源。") add_table(doc, ["阶段", "主要任务", "完成判断"], [ ["1. 主体与代码", "全国院校、外部招生代码、别名和校区", "学校稳定 ID 清晰,未匹配主体已形成处理结论"], ["2. 结构与规则", "批次段次、计划发布批次、院校专业组", "省年口径、组代码和选科要求可验证"], ["3. 数量与分数", "专业计划、专业录取线、一分一段、省控线、投档线", "关键值与来源一致,异常有备注"], ["4. 覆盖与证据", "覆盖缺口、来源档案、内容哈希", "每类数据的完整性和证据链均有说明"], ], widths=[3.0, 6.5, 6.0], font_size=8.7) add_heading(doc, "8.2 每个省年的建议批次", 2) add_number(doc, "锁定省份和年份,记录本次审核范围、责任人和开始日期。") add_number(doc, "查看覆盖与缺口,确定有哪些数据集、哪些为空、哪些仅部分覆盖。") add_number(doc, "先筛选“未匹配”,处理院校和专业组关联问题。") add_number(doc, "按批次和科类抽样核对计划、控制线、投档线和专业录取线。") add_number(doc, "检查一分一段的连续性和累计人数逻辑。") add_number(doc, "核对来源标题、发布方、日期、网页和哈希,补充缺口备注。") add_number(doc, "回到审核状态筛选,确认“需复核”和“已驳回”都有明确的后续动作。") add_heading(doc, "8.3 抽样与全量审核", 2) add_para(doc, "关键控制线、来源档案、学校映射和异常记录建议逐条审核。数量庞大的计划和录取线可先按来源、批次、科类分层抽样,再对异常值、未匹配项和抽样失败的同批数据扩大检查。当前界面以逐条审核为主;不要把筛选结果误认为已批量改变状态。") add_heading(doc, "9. 覆盖与来源管理", 1) add_heading(doc, "9.1 覆盖状态的审核方法", 2) add_number(doc, "确认省份、年份、数据集、科类和段次是否与备注描述一致。") add_number(doc, "比较记录数与来源文件/页面的预计规模,注意分页、隐藏行和多个工作表。") add_number(doc, "若只是部分批次或部分科类,选择“公开部分数据”,并精确写明缺口。") add_number(doc, "只有在证据显示公开范围已完整纳入时,才使用“公开完整快照”。") add_number(doc, "未找到全量公开数据时记录检索范围和日期,使用“暂无公开全量”,而不是制造空数据。") add_heading(doc, "9.2 来源档案页面", 2) add_figure(doc, "05-sources.png", "图 5 来源档案:集中保存发布方、标题、日期、落地页与内容哈希") add_table(doc, ["来源类型", "含义", "审核重点"], [ ["official_xls", "官方发布的 Excel/表格文件", "文件版本、工作表、发布时间、哈希"], ["official_html", "官方网页或查询页面", "页面标题、发布日期、参数和可访问性"], ["official_republication_html", "官方或权威渠道转载的页面", "是否保留原始出处,内容是否完整"], ["open_dataset_csv", "公开数据集 CSV", "许可、生成方式、字段映射、与官方口径差异"], ], widths=[4.1, 5.1, 6.3], font_size=8.6) add_heading(doc, "9.3 来源证据链", 2) add_para(doc, "一条可靠证据链至少应能回答:谁发布、何时发布、发布了什么、原始网页在哪里、下载文件是哪一版、内容是否发生变化、哪些业务记录由它生成。审核时优先打开落地页;本地文件存在时再核对文件名、大小、哈希和解析范围。") add_note(doc, "网页失效时:", "不要删除已有来源档案。保留标题、发布日期、哈希和本地文件信息,并在审核备注中记录访问失败日期。") add_heading(doc, "10. 常见审核场景", 1) add_heading(doc, "10.1 审核某省某年的全部数据", 2) add_number(doc, "从“省份与年份”选择目标省年,截图或记录初始状态。") add_number(doc, "打开“覆盖与缺口”,列出已采集、部分采集和暂无公开全量的数据集。") add_number(doc, "按第 8 章顺序进入各数据集,优先筛选待审核和未匹配。") add_number(doc, "每完成一类,检查需复核/驳回记录的备注是否包含证据和后续动作。") add_number(doc, "完成后回到矩阵,由负责人根据既定验收口径更新范围状态。") add_heading(doc, "10.2 定位未匹配院校", 2) add_number(doc, "进入院校专业组、专业计划、专业录取线或投档线页面。") add_number(doc, "将“匹配状态”设为“未匹配”,再按省年和批次缩小范围。") add_number(doc, "核对招生代码、学校名称、历史名称和同名院校,查看全国院校的外部代码映射。") add_number(doc, "证据明确后再修复映射;审核页面本身只保存审核结论,不直接改写映射。") add_heading(doc, "10.3 核对一条分数线", 2) add_number(doc, "确认省份、年份、科类、层次、批次和段次。") add_number(doc, "确认该记录是省控线、院校/专业组投档线还是专业录取线,避免类别混淆。") add_number(doc, "比较分数、位次、志愿序号或专业分等字段,并确认空值是否来自源数据。") add_number(doc, "打开来源页面定位原表位置;存在版本差异时记录版本和发布日期。") add_number(doc, "提交已通过、需复核或已驳回,并写明依据。") add_heading(doc, "10.4 核对数据导入结果", 2) add_bullet(doc, "先看首页数量是否与导入预期接近,再按省年进入列表检查。") add_bullet(doc, "检查首尾分数、随机中段、特殊字符、前导零代码、长备注和空值。") add_bullet(doc, "检查来源档案和覆盖记录是否同步建立;只有业务表有数据并不代表导入完整。") add_heading(doc, "11. 故障排查与常见问题", 1) add_table(doc, ["现象", "可能原因", "处理方法"], [ ["页面无法打开", "前端未启动或端口被占用", "确认 5173 服务运行;检查地址和端口"], ["页面能打开但列表报错/为 0", "后端未启动、数据库连接失败", "确认 8080 和 MySQL 3306;查看服务日志"], ["筛选后无结果", "省年、科类或审核状态组合过窄", "点击重置,再逐项增加条件"], ["保存审核失败", "审核人为空、状态无效、后端或数据库异常", "补全审核人;刷新后重试;仍失败则记录时间与记录 ID"], ["来源网页打不开", "页面下线、参数变化或网络限制", "保留档案与哈希;尝试官方站点检索同标题;记录访问失败"], ["列表滚动或加载较慢", "每页数量过大或筛选范围太宽", "改为 20/50 条并增加省年、批次或关键词"], ["代码前导零丢失", "源文件按数值列解析", "标记需复核,回查原文件;代码字段应按文本处理"], ["重复记录", "多版本或重复来源导入", "比较省年、批次、业务键、来源和哈希后再判断"], ], widths=[3.6, 5.0, 6.9], font_size=8.45) add_heading(doc, "11.1 常见问题", 2) add_para(doc, "问:灰色省份是否表示官方没有数据?", bold_lead="问:") add_para(doc, "答:不是。灰色只表示当前系统尚未采集该省年数据。应创建采集任务并在公开渠道继续检索。", bold_lead="答:") add_para(doc, "问:提交“已通过”会修改分数或计划数吗?", bold_lead="问:") add_para(doc, "答:不会。审核结论与原始业务数据分开保存,并新增审计日志。", bold_lead="答:") add_para(doc, "问:为什么记录数很多,但覆盖状态仍是“部分数据”?", bold_lead="问:") add_para(doc, "答:记录量不等于覆盖完整。可能仍缺批次、科类、征集轮次、部分学校或关键字段。", bold_lead="答:") add_para(doc, "问:找不到来源时能否凭其他网站补齐?", bold_lead="问:") add_para(doc, "答:可以把权威转载或开放数据作为线索,但必须登记来源类型和差异;关键结论仍应尽量回溯官方证据。", bold_lead="答:") add_heading(doc, "11.2 反馈问题时应提供", 2) add_bullet(doc, "发生时间、操作页面、目标省份和年份。") add_bullet(doc, "记录 ID 或稳定 ID、使用的筛选条件、审核状态。") add_bullet(doc, "页面截图、错误提示、来源标题和来源地址。") add_bullet(doc, "问题能否复现,以及复现所需的最短步骤。") add_heading(doc, "附录 A:审核前后检查清单", 1) add_heading(doc, "A.1 开始前", 2) for text_ in [ "已确认本次省份、年份、数据集和批次范围。", "已查看覆盖与缺口,知道哪些数据尚未收集或仅部分采集。", "已确认来源档案可用,或已记录网页失效情况。", "已明确抽样/全量策略和责任人。", ]: add_bullet(doc, "□ " + text_) add_heading(doc, "A.2 单条记录", 2) for text_ in [ "省份、年份、科类、层次、批次和段次上下文正确。", "院校、专业组、专业代码与稳定 ID 关联合理。", "分数、位次、计划数和人数与来源一致。", "空值、复合分值和备注没有被错误解释。", "来源标题、发布日期、网页和哈希足以支持结论。", "审核状态选择恰当,备注包含必要证据和后续动作。", ]: add_bullet(doc, "□ " + text_) add_heading(doc, "A.3 完成后", 2) for text_ in [ "需复核和已驳回记录均有明确原因。", "覆盖记录与实际导入范围一致。", "来源档案没有遗漏,重复来源已通过哈希辨别。", "已刷新页面并抽查审核状态与历史记录。", "已向后续负责人移交未完成项。", ]: add_bullet(doc, "□ " + text_) add_heading(doc, "附录 B:字段速查", 1) add_table(doc, ["字段/术语", "说明"], [ ["稳定 ID", "系统内部用于跨来源、跨年份关联同一业务主体的标识"], ["外部代码", "教育部代码、各省招生代码或来源系统代码,需注明适用范围"], ["专业组", "新高考招生组织单元,通常带选科要求和组代码"], ["最低位次", "在指定省年和科类口径下,与最低分对应的排名位置"], ["同分人数", "该分值上的考生数量"], ["累计人数", "达到该分值及以上的累计人数,具体以省级公布口径为准"], ["内容哈希", "对来源文件内容计算的指纹,用于识别重复和变更"], ["覆盖状态", "描述公开数据可得性与当前采集完整性,不等同于审核状态"], ["审核状态", "描述单条记录的业务审核结论,不直接修改原始值"], ], widths=[4.0, 11.5], font_size=8.8) add_heading(doc, "附录 C:使用边界", 1) add_para(doc, "本系统是数据采集和审核工具,不替代各省教育考试院、教育部阳光高考平台或高校官方发布。面向考生展示或用于志愿决策前,应再次校验最新政策、招生章程、选科要求、计划变更和正式录取口径。") add_para(doc, "本文档中的数量为编制时系统快照,仅用于说明界面和操作。数据重新导入、覆盖或审核后,首页指标会实时变化。") # Global paragraph properties and document metadata doc.core_properties.title = "志愿通数据审核系统使用手册" doc.core_properties.subject = "数据查看、来源追溯、审核记录和覆盖管理" doc.core_properties.author = "志愿通项目组" doc.core_properties.keywords = "志愿通, 数据审核, 高考招录, 使用手册" for p in doc.paragraphs: if p.style and p.style.name.startswith("Heading"): p.paragraph_format.widow_control = True OUT.parent.mkdir(parents=True, exist_ok=True) doc.save(OUT) print(OUT)