# 接口一览 所有接口统一走 `api.php?action=`,响应为 JSON: ```jsonc // 成功 { "ok": true, "data": { ... } } // 失败 { "ok": false, "error": "错误信息" } ``` - 未登录时返回 `{ "ok": false, "error": "...", "auth": false }`,前端会清会话并跳登录页; - **写操作**必须在 JSON 体里带 `_csrf` 字段(或 `X-CSRF-Token` 头), 令牌从 `config_get` 接口获取。 ## 写操作白名单 `config_save`、`roster_save`、`roster_import`、`roster_clear`、 `unmatched_map`、`project_config_save` ## 接口清单 ### 认证与配置 | Action | 方法 | 参数 | 说明 | | --- | --- | --- | --- | | `auth_check` | GET | — | 是否已登录 | | `config_get` | GET | — | 返回配置(密码不回显)+ `csrf_token` + `storage` / `db_driver` / `db_connected` | | `config_save` | POST | JSON 配置对象 | 保存;返回 `conn_changed` 与 `ping` 结果 | | `config_test` | POST | 连接信息 | 测试连接,不落库 | | `ping` | GET | — | 探测卷王是否可达 | | `diag` | GET | — | 诊断信息(存储后端、DB 状态等) | > **只有连接信息(地址 / 前缀 / 账号 / 密码 / 证书校验)变化时**,`config_save` 才会清除令牌并重新登录。 ### 数据 | Action | 方法 | 参数 | 说明 | | --- | --- | --- | --- | | `projects` | GET | — | 卷王项目(问卷 / 考试)列表 | | `data` | GET | `projectId`、`nameQid`、`force` | 主系统数据:统计、成绩行、名单、缺考、未匹配 | | `dashboard` | GET | `projectId`、`nameQid`、`force`、`class` | 按班级 / 组别聚合的看板数据。`class` 非空且未登录时,服务端只回这一个班(并标记 `public`) | `data` 响应关键字段: ``` stats { total, submitted, absent, rate, avg, pass_rate, max, min, avg_duration } rows[] { sid, name, cls, grp, score, best_score, attempts, attempts_list[], submit_ts, submit_at, last_submit_ts, duration, duration_txt } roster[] { sid, name, cls, grp, login } absent[] 缺考名单 unmatched[] 未匹配答卷 project_cfg 本卷配置 _cache { cached, at } ``` ### 名单 | Action | 方法 | 参数 | 说明 | | --- | --- | --- | --- | | `roster` | GET | — | 名单与元信息 | | `roster_import` | POST | `multipart`:`file`、`mode=replace\|append` | 导入(扩展名白名单 + 随机名 + 解析后即删) | | `roster_save` | POST | JSON `{ students: [...] }` | 全量替换(DELETE + INSERT) | | `roster_clear` | POST | — | 清空 | | `unmatched_map` | POST | JSON `{ name, sid, cls, grp, login }` | 未匹配答卷补入名单(学号已存在则更新姓名) | ### 每卷配置 | Action | 方法 | 参数 | 说明 | | --- | --- | --- | --- | | `project_config` | GET | `projectId` | 返回 `cfg`、`effective`(生效值)、`global`(全局默认) | | `project_config_save` | POST | JSON `{ full_score, pass_rate, exempt[] }` | 留空/0 = 清除该项,回落全局 | ### 导出 | Action | 方法 | 参数 | 说明 | | --- | --- | --- | --- | | `export` | GET | `projectId`、`nameQid`、`cls`、`grp`、`mode=latest\|all`、`scoreMode=latest\|best`、`split=cls`、`sort`、`cols`、`skipTemp=1`、`noAbsent=1` | 成绩明细 `.xlsx`(`all` 时每人每次考试一行) | | `export_absent` | GET | `projectId`、`nameQid`、`cls`、`grp` | 缺考名单 `.xlsx` | | `export_multi` | GET | `projectIds`(逗号分隔)、`nameQid`、`mode=block\|summary\|both`、`wide=1`、`passTimes`、`markBest=0`、`passRow=0`、`scoreMode=latest\|highest`、`cls` / `grp`(可多值)、`withCls` / `withGrp`、`includeTemp`、`format=json` | 「每次考试」块表 / 宽表 / 成绩对比 `.xlsx`(`format=json` 时返回装配结果供前端打印) | | `draw_export` | GET | `type=import\|list` | 白名单导入模板 / 白名单列表 | `sort` 取值:`time` / `sid` / `score_desc` / `score_asc`。 ### 导出新参数 | 参数 | 适用接口 | 取值 | 说明 | | --- | --- | --- | --- | | `mode=all` | `export` | 缺省 | 每次考试一行(同一学号考 N 次就有 N 行);`mode=latest` 为每人一行最新成绩 | | `scoreMode=best` | `export` | 缺省 | 每人一行的代表成绩改取**最高成绩**(默认 `latest` = 最近一次真实提交);`mode=all` 不生效 | | `noAbsent=1` | `export`、`export_multi` | 缺省 | 不附缺考名单工作表(`export_multi` 口径:所选考试全都没交的人) | | `skipTemp=1` | `export` | 缺省 | `mode=all` 时排除暂存草稿;`includeTemp=1` 可强制保留 | | `mode=block` | `export_multi` | **默认** | 两行表头的「每次考试」块表:一列「第几次」被所有考试共用,每场考试占 2 列(考试名横跨 2 列 + 第二行「成绩」小标题),学生块末尾一行「合格次数」,学号·姓名纵向合并,没考的考试合并成「未考」(旧值 `merged` / `attempts` 等价) | | `wide=1` | `export_multi` | — | 只传 1 个 `projectId` 时改用宽表:每人一行、第几次直接做列名(`学号 / 姓名 / 第1次…第N次 / [合格次数]`),由 `ny_assemble_attempt_wide_sheet()` 生成 | | `passTimes` | `export_multi` | 1 | 「合格次数」达标阈值:该场达到合格线的次数 ≥ 此值才标绿(默认 1) | | `markBest=0` | `export_multi` | 开 | `=0` 取消「最高分标红」 | | `passRow=0` | `export_multi` | 开 | `=0` 不输出「合格次数」行(也就没有绿底) | | `mode=summary` | `export_multi` | — | 成绩对比:一行一个学生、一列一场考试(旧值 `matrix` 等价) | | `mode=both` | `export_multi` | — | 第一张块表 + 第二张成绩对比表 | | `format=json` | `export_multi` | — | 不下载文件,`ny_ok` 返回 `sheets`(`rows` / `merges` / `widths` / `header_rows` / `cell_styles`)+ `meta`(考试名、场次、班级 / 组别、列开关、`pass_line` / `pass_times` / `mark_best` / `pass_row` / `wide`),供「多考试打印」渲染 | | `cls` / `grp` | `export_multi` | 新增 | 支持多选(逗号分隔),空 = 全部;与单考试 `export` 的筛选口径一致 | | `withCls` / `withGrp` | `export_multi` | 新增 | 强制出 / 不出班级、组别列;缺省由 `ny_multi_col_flags()` 判定:选 ≥2 个班级(组别)才出,只选 1 个班级且未选组别两列都不出 | | `includeTemp=1` | `export_multi` | 缺省 | 不传 = 块表默认跳过暂存草稿(保留时标注 `第N次(草稿)`) | 块表由 `ny_assemble_block_sheet()` 生成,返回 `rows`(两行表头 + 学生块)/ `merges` / `widths` / `header_rows=2` /`cell_styles`;宽表由 `ny_assemble_attempt_wide_sheet()` 生成,参数同上。 合格线按**每场各自**的 `ny_resolve_score_cfg($pid, $c)` 解析(本卷满分 × 合格率),不是全场统一一条线。 `cell_styles` 是 `行,列 → red | green`(行与列均 **1-based**),只有两类标记: - `red` —— 该场该生成绩 = 该场最高分(同分只标最早达成的那次); - `green` —— 该场该生成绩达到合格线的次数 ≥ `passTimes`(未达标不标记)。 `XlsxWriter::renderMulti()` 支持 `merges`(1-based 闭区间 `[r1,c1,r2,c2]`)、`header_rows`(多行表头同样式) 与 `cell_styles`(单元格级红字 / 绿底);被合并的非首格写成「空值 + 同样式」以保证合并区域四周边框完整, 并跟随左上角首格的标记,避免绿底只覆盖半个格子。 前端 `sheetToHtml()` 把同一份 `merges` 换算成 `colspan` / `rowspan`、把 `cell_styles` 换算成红字 / 绿底, 所以**打印与导出的 Excel 用同一套判定**。 列显示规则 `ny_multi_col_flags()` 与排序比较函数同样放在 `lib/multi_export.php`(纯函数,可单测)。 ## 前端调用约定 ```js // 读 const d = await api('data', { params: { projectId, force: 1 } }) // 写:api() 会自动注入 _csrf await api('roster_save', { json: { students } }) ``` ## 相关 - [架构与目录](./architecture.md) - [测试](./testing.md)