Skip to content

试卷 JSON 标准 v1

本文是前端编辑器、创建试卷接口、生成器和扫描器共同使用的数据标准。

核心原则:

  • 每道小题是独立对象,不再用 columns / rows 自动对齐整块选择题。
  • 试卷名不写在 JSON 内;名称由数据库的 exam 列和创建接口的 name 参数管理。
  • 涂卡题使用 type: "mark",其他题使用 type: "other"。
  • 扫描不识别图像上的题号;结果按 questions[].id 返回。
  • other 题的 region 就是裁切区域,后续可直接裁给主观题处理流程。
  • 页面坐标统一使用 0..1 比例,原点在左上角。
  • 涂卡题的选项坐标相对小题区域,不相对整页。

机器可读版本见同目录的 exam.schema.json。完整可提交示例见 exam.example.json。


1. 完整示例

{
  "schemaVersion": 1,
  "unit": "ratio",
  "paper": {
    "name": "A4",
    "width": 1240,
    "height": 1754,
    "physical": {
      "width": 210,
      "height": 297
    },
    "pdf": {
      "width": 595.28,
      "height": 841.89
    }
  },
  "markers": [
    { "id": "top-left", "x": 0.03, "y": 0.03, "size": 0.035 },
    { "id": "top-right", "x": 0.935, "y": 0.03, "size": 0.035 },
    { "id": "bottom-left", "x": 0.03, "y": 0.935, "size": 0.035 },
    { "id": "bottom-right", "x": 0.935, "y": 0.935, "size": 0.035 }
  ],
  "blocks": [
    {
      "id": "b1",
      "type": "text",
      "region": { "x": 0.08, "y": 0.05, "width": 0.55, "height": 0.05 },
      "text": "高一数学第一次月考",
      "style": {
        "fontSize": 0.04,
        "fontWeight": "bold",
        "textAlign": "left"
      }
    },
    {
      "id": "b2",
      "type": "text",
      "region": { "x": 0.08, "y": 0.09, "width": 0.55, "height": 0.03 },
      "text": "考试名称:____________________",
      "style": { "fontSize": 0.014 }
    },
    {
      "id": "b3",
      "type": "text",
      "region": { "x": 0.08, "y": 0.12, "width": 0.55, "height": 0.03 },
      "text": "姓名:________  班级:________",
      "style": { "fontSize": 0.014 }
    },
    {
      "id": "b4",
      "type": "text",
      "region": { "x": 0.45, "y": 0.12, "width": 0.5, "height": 0.08 },
      "text": "注意事项:\n1. 请用 2B 铅笔填涂,修改时用橡皮擦干净。\n2. 每题只能填涂一个选项,多涂不得分。",
      "style": {
        "fontSize": 0.012,
        "lineHeight": 1.5
      }
    }
  ],
  "questions": [
    {
      "id": "q1",
      "displayNumber": 1,
      "type": "mark",
      "region": {
        "x": 0.06,
        "y": 0.4,
        "width": 0.38,
        "height": 0.05
      },
      "choices": [
        { "value": "A", "region": { "x": 0, "y": 0, "width": 0.2, "height": 1 } },
        { "value": "B", "region": { "x": 0.25, "y": 0, "width": 0.2, "height": 1 } },
        { "value": "C", "region": { "x": 0.5, "y": 0, "width": 0.2, "height": 1 } },
        { "value": "D", "region": { "x": 0.75, "y": 0, "width": 0.2, "height": 1 } }
      ]
    },
    {
      "id": "q13",
      "displayNumber": 13,
      "type": "other",
      "region": {
        "x": 0.06,
        "y": 0.6,
        "width": 0.88,
        "height": 0.18
      }
    }
  ],
  "barcode": {
    "id": "sheetCode",
    "type": "code128",
    "value": "20261109001",
    "anchor": "top-right",
    "offset": { "unit": "mm", "x": 22, "y": 10 },
    "size": { "unit": "mm", "width": 45, "height": 12 },
    "minSize": { "width": 30, "height": 8 }
  },
  "answerKey": {
    "q1": "A"
  }
}

2. 顶层字段

字段 必填 类型 说明
schemaVersion 是 1 当前固定为 1
unit 是 "ratio" 坐标单位,固定为页面比例
paper 是 object 纸张和渲染尺寸
markers 是 array 四角定位块,必须正好 4 个
questions 是 array 小题列表,至少 1 个
blocks 否 array 自定义展示内容块,不参与识别
barcode 否 object 条形码布局
answerKey 否 object 答案键,格式为 { "q1": "A" }

试卷名不进入 JSON。它由创建接口的 name 参数写入数据库的 exam 列;同名创建直接返回 409,不会覆盖旧记录。

2.1 自定义内容块 blocks

title、examLine、infoLine、notes 这类固定字段不再进入标准。标题、考试信息、注意事项、说明文字等,全部由编辑者自己决定:

{
  "blocks": [
    {
      "id": "b1",
      "type": "text",
      "region": { "x": 0.08, "y": 0.05, "width": 0.55, "height": 0.05 },
      "text": "高一数学第一次月考",
      "style": {
        "fontSize": 0.04,
        "fontWeight": "bold",
        "textAlign": "left"
      }
    }
  ]
}

规则:

  • id 是编辑器内部标识,可以随意取名,不要求语义,也不参与扫描;
  • 当前只支持 type: "text",后续可以扩展其他块类型;
  • region 使用页面比例,作者决定放在哪里、多大;
  • text 支持换行,注意事项可以写成一个多行块,也可以拆成多个块;
  • style 可选,支持 fontSize(相对页面短边)、fontWeight、textAlign、lineHeight、color、fontFamily;
  • blocks 不属于 questions,没有选项、没有答案键、不会进入识别结果;
  • blocks[].id 不能与任何 questions[].id 重复。

3. 纸张对象

{
  "name": "A4",
  "width": 1240,
  "height": 1754,
  "physical": { "width": 210, "height": 297 },
  "pdf": { "width": 595.28, "height": 841.89 }
}
字段 说明
name 纸张名,用于输出文件名,例如 答题卡-空白-A4.pdf
width / height 渲染画布像素尺寸,用于 SVG/PNG 预览
physical.width / physical.height 实际纸张毫米尺寸,用于 mm 单位换算
pdf.width / pdf.height PDF 页面尺寸,单位是 PDF point

4. 定位块

{
  "id": "top-left",
  "x": 0.03,
  "y": 0.03,
  "size": 0.035
}
  • id 必须正好包含 top-left、top-right、bottom-left、bottom-right。
  • x、y 是定位块左上角的页面比例。
  • size 是正方形边长相对页面短边的比例。
  • 定位块用于扫描时计算页面透视变换。

5. 小题对象

每道题都有独立的 id、type 和 region。题目之间没有全局行列关系。

5.1 公共字段

字段 必填 类型 说明
id 是 string 全局唯一,例如 q1,扫描结果使用这个 key
displayNumber 否 integer 只用于打印显示;不参与识别
type 是 "mark" 或 "other" 小题类型
region 是 object 题目在页面中的区域

region:

{
  "x": 0.06,
  "y": 0.4,
  "width": 0.38,
  "height": 0.05
}
  • 使用页面比例,原点左上角。
  • 必须满足 x + width <= 1、y + height <= 1。
  • region 同时是题目的整体定位区域;主观题直接拿它裁切。

5.2 涂卡题 type: "mark"

{
  "id": "q1",
  "displayNumber": 1,
  "type": "mark",
  "region": { "x": 0.06, "y": 0.4, "width": 0.38, "height": 0.05 },
  "choices": [
    { "value": "A", "region": { "x": 0, "y": 0, "width": 0.2, "height": 1 } },
    { "value": "B", "region": { "x": 0.25, "y": 0, "width": 0.2, "height": 1 } },
    { "value": "C", "region": { "x": 0.5, "y": 0, "width": 0.2, "height": 1 } },
    { "value": "D", "region": { "x": 0.75, "y": 0, "width": 0.2, "height": 1 } }
  ]
}

choices[].region 相对 question.region,不是相对页面:

页面绝对区域 =
  x: question.region.x + choice.region.x * question.region.width
  y: question.region.y + choice.region.y * question.region.height
  width: choice.region.width * question.region.width
  height: choice.region.height * question.region.height

这样拖动整道题时,A/B/C/D 会一起移动。

注意:

  • 一道涂卡题必须配置 choices;没有 choices 时只能判断“这个区域被涂过”,无法判断涂的是 A/B/C/D。
  • choices[].value 在同一小题内不能重复。
  • 扫描时只采样每个 choice.region,不识别题号。
  • 结果可能是 blank、single、multi、all,由检测配置决定阈值。

识别结果示例:

{
  "q1": {
    "type": "mark",
    "answer": "A",
    "mode": "single"
  }
}

5.3 其他题 type: "other"

{
  "id": "q13",
  "displayNumber": 13,
  "type": "other",
  "region": { "x": 0.06, "y": 0.6, "width": 0.88, "height": 0.18 }
}
  • 不配置 choices。
  • 目前作为主观题处理:扫描时按 region 裁切,不做答案识别。
  • 后续大题的裁切、人工批改、OCR/大模型处理都可以直接使用这个区域。
  • 输出示例:
{
  "q13": {
    "type": "other",
    "crop": "q13.png"
  }
}

6. 条形码

{
  "id": "sheetCode",
  "type": "code128",
  "value": "20261109001",
  "anchor": "top-right",
  "offset": { "unit": "mm", "x": 22, "y": 10 },
  "size": { "unit": "mm", "width": 45, "height": 12 },
  "minSize": { "width": 30, "height": 8 }
}
  • anchor:top-left、top-right、bottom-left、bottom-right。
  • offset:相对锚点的偏移;单位可以是 mm 或 ratio。
  • size:条形码尺寸;单位可以是 mm 或 ratio。
  • minSize:物理最小尺寸,单位固定为毫米;小于该尺寸时生成阶段报错。
  • type 默认 code128,value 是要编码的内容。

条形码的位置由“锚点 + 偏移 + 尺寸”解析,不依赖页面其他元素。


7. 答案键

答案键按小题 id 索引,不使用显示题号:

{
  "answerKey": {
    "q1": "A",
    "q2": "B",
    "q13": "C"
  }
}

规则:

  • 只能引用 type: "mark" 的小题。
  • 值必须存在于该题的 choices[].value 中。
  • type: "other" 的小题不进入答案键。

如果前端只做创建,不涉及批改,可以暂时省略 answerKey;生成标准答案或扫描批改时再补。


8. 校验规则

创建空试卷时校验:

  1. name 非空、1–100 字符、不能有首尾空白、不能包含 Windows 文件名非法字符。
  2. name 在数据库中不能重复。

PUT 提交内容时校验:

  1. paper、markers、questions 完整;blocks 可选。
  2. 四个定位块 id 不重复且齐全。
  3. 每个 region 在页面范围内,width/height > 0。
  4. 涂卡题至少有 2 个选项,选项值不重复。
  5. questions[].id 和 blocks[].id 全局唯一。
  6. blocks[].type 当前只能是 "text",文本不能为空。
  7. answerKey 只能引用存在的涂卡题和存在的选项。
  8. 顶层对象不允许未声明字段,避免前端拼错字段却被静默保存。
  9. JSON 顶层不允许出现 name;试卷名只在数据库和创建接口中维护。
  10. 不再接受 title、testNotice、examLine、infoLine、notes 这类固定字段。

9. 前端编辑器的数据流

POST /api/v1/exams { name }       创建空试卷,拿到 id
  → GET /api/v1/exams/{id}        读取当前内容(初始为 {})
  → 编辑 paper / markers / questions / blocks
  → 涂卡题编辑 region 和 choices
  → 主观题只编辑 region
  → 内容块自定义 id、region、text、style
  → 可选编辑 barcode / answerKey
  → PUT /api/v1/exams/{id}        保存完整内容
  → POST /api/v1/files            上传待检查的答题卡图片,拿到 fileId
  → POST /api/v1/exams/check { examId, fileIds } 批量检查

编辑器不需要计算全局行列,不需要自动对齐,也不需要识别题号。每道题就是画布上的一个独立对象。


10. 当前实现状态

  • POST /api/v1/exams 创建空试卷并去重。
  • GET / PUT / DELETE /api/v1/exams/:id 已实现。
  • POST /api/v1/exams/check 已按 questions 数组批量识别涂卡题;other 题目前只返回类型,不做识别。
  • 仓库中现有的 generate / simulate 仍读取旧的 config/exam.json 的 fields 网格。
  • 后续把生成、填涂接入新标准时,统一以 questions 数组为准;旧的 fields 只保留给现有 /api/ocr 测试流程。