有关志愿通的代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

730 lines
41 KiB

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)