比赛
比赛(Contest)
目录
比赛资源管理比赛会话——即比赛模块把 QSO 归组为一次比赛过程的方式——以及会话与其 QSO 之间的关联。它是 Web 界面中比赛页面的 API 对应物,让外部工具(日志软件、离线镜像、同步客户端)能够读取并复制比赛会话,而不必通过导入历史比赛手工重建。
- 基础路径:
/api/v2/contest - 作用域:
contest:read、contest:write、contest:delete - 起始版本: 3.2.0
注意
请先阅读 API v2 概览,了解认证、响应封装与错误码。
端点
| 动词 | 路径 | 作用域 | 用途 | 起始版本 |
|---|---|---|---|---|
GET |
/api/v2/contest |
contest:read |
列出令牌所有者的比赛会话 | 3.2.0 |
GET |
/api/v2/contest/{id} |
contest:read |
单个会话,包括其关联的 QSO id | 3.2.0 |
POST |
/api/v2/contest |
contest:write |
创建会话,可选地关联 QSO | 3.2.0 |
PATCH |
/api/v2/contest/{id} |
contest:write |
部分更新;关联与取消关联 QSO | 3.2.0 |
DELETE |
/api/v2/contest/{id} |
contest:delete |
删除会话 | 3.2.0 |
属于其他用户的会话返回 404 not_found,绝不会返回 403——API 从不确认外部 id 是否存在。
会话对象
{
"id": 17,
"contest": "DARC-WAG",
"contest_name": "Worked All Germany Contest",
"time_start": "2026-10-17 15:00:00",
"time_end": "2026-10-18 15:00:00",
"station_id": 3,
"comment": "SO ABHP",
"settings": {
"exchangetype": "Serialexchange",
"copyexchangeto": "dok",
"exchangefields": ["serial", "exchange"],
"callbook_lookup": true,
"custom_name": "",
"serial_per_band": false,
"serial_scope": "station"
},
"qso_count": 412,
"created_at": "2026-10-17 14:52:11",
"updated_at": "2026-10-18 15:04:37"
}
| 字段 | 含义 |
|---|---|
id |
会话 id,在本 Wavelog 实例内保持稳定 |
contest |
ADIF 比赛名称,例如 DARC-WAG——用于定位一场比赛的取值,见比赛名称 |
contest_name |
来自目录的可读比赛名称 |
time_start / time_end |
会话时间窗口,YYYY-MM-DD HH:MM:SS |
station_id |
会话所属的台站位置 |
comment |
自由文本备注 |
settings |
会话设置,见下文 |
qso_count |
当前关联到该会话的 QSO 数量 |
created_at / updated_at |
由 Wavelog 维护的时间戳 |
提示:用 ADIF 名称定位比赛,而不是 id
数字形式的比赛目录 id 是实例本地的——id
42在另一个 Wavelog 上可能是另一场比赛。写请求接受contest_id作为替代,但在实例之间同步的客户端应始终使用contest。响应中同时带有 ADIF 名称和解析出的目录名称。可用的比赛由
GET /api/v2/catalog?topic=contest提供——无需任何作用域,因此任何有效令牌都能填充比赛下拉菜单。
比赛名称
同一个取值有三种不同的叫法,很容易搞混:
| 位置 | 叫法 |
|---|---|
| 本 API | contest |
| Web 界面,比赛 → 比赛管理 | ADIF 名称(编辑表单中为 比赛 ADIF 名称) |
| 导出的 ADIF 记录 | CONTEST_ID |
因此,管理员以 ADIF 名称 = DARC-WAG 创建的比赛,在这里通过 "contest": "DARC-WAG" 定位;从此类会话导出的每条 QSO 都带有 <CONTEST_ID:8>DARC-WAG。
警告:
CONTEST_ID不是contest_id
CONTEST_ID是该文本取值的 ADIF 字段名。本 API 的contest_id完全是另一回事:数字形式的目录 id。发送{"contest_id": "DARC-WAG"}会以400 validation_error失败,{"contest": 57}同样如此。
会话设置
settings 是一个 JSON 对象。读取时它总是完整返回,并在模块默认值之上合并,因此客户端永远不必知道存储了哪些键。写入时只校验你发送的键,并将其合并到现有取值之上。
| 键 | 默认值 | 取值 |
|---|---|---|
exchangefields |
["serial"] |
serial、gridsquare、exchange 的非空数组 |
exchangetype |
Serial |
派生字段,见下文 |
copyexchangeto |
"" |
""、dok、locator、qth、name、age、state、power |
callbook_lookup |
true |
布尔值 |
custom_name |
"" |
字符串 |
serial_per_band |
false |
布尔值 |
serial_scope |
"station" |
station 或 operator |
exchangefields 是数组,因此这里是 API 通常的仅标量请求体规则唯一不适用之处——settings、qso_ids、link_qso_ids 和 unlink_qso_ids 可以嵌套,其他所有字段必须保持标量。
exchangetype 是派生的历史遗留字段。你发送的任何值都会被忽略,并由 exchangefields 重新计算,与 Web 界面的做法完全一致,因此两者永远不会偏离:
exchangefields |
生成的 exchangetype |
|---|---|
["serial"] |
Serial |
["exchange"] |
Exchange |
["serial", "exchange"] |
Serialexchange |
["serial", "gridsquare"] |
Serialgridsquare |
["exchange", "gridsquare"] |
Exchangegridsquare |
["serial", "gridsquare", "exchange"] |
SerialGridExchange |
未知的设置键或无效取值会返回 400 validation_error,各条消息位于 error.details.errors 中。
列出会话
curl "https://<WAVELOG_URL>/index.php/api/v2/contest" \
-H "Authorization: Bearer wl2_your_token_here"
会话按最新优先返回(先按 time_start,再按 id)。该列表不分页,也不包含 qso_ids——需要时请获取单个会话。
| 参数 | 默认值 | 含义 |
|---|---|---|
station_id |
令牌用户拥有的全部台站 | 逗号分隔的台站 id;会做归属校验 |
since_id |
0 |
仅返回 id 大于此值的会话——用于增量同步 |
令牌不拥有的 station_id 会以 403 forbidden 拒绝,而不是静默丢弃。
获取单个会话
curl "https://<WAVELOG_URL>/index.php/api/v2/contest/17" \
-H "Authorization: Bearer wl2_your_token_here"
响应是会话对象再加上 qso_ids,即已关联 QSO 主键的排序列表——与 QSO 资源使用的 id 相同,因此客户端可以镜像该关联关系:
{
"data": {
"id": 17,
"contest": "DARC-WAG",
"qso_count": 3,
"qso_ids": [48190, 48200, 48215]
}
}
创建会话
curl -X POST "https://<WAVELOG_URL>/index.php/api/v2/contest" \
-H "Authorization: Bearer wl2_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"contest": "DARC-WAG",
"time_start": "2026-10-17 15:00",
"time_end": "2026-10-18 15:00",
"station_id": 3,
"comment": "SO ABHP",
"settings": {
"exchangefields": ["serial", "exchange"],
"copyexchangeto": "dok"
}
}'
| 字段 | 必填 | 说明 |
|---|---|---|
contest 或 contest_id |
是 | ADIF 比赛名称(推荐)或目录 id |
time_start |
是 | YYYY-MM-DD HH:MM 或 YYYY-MM-DD HH:MM:SS |
time_end |
是 | 相同格式 |
station_id |
是 | 必须属于令牌所有者 |
comment |
否 | 默认为空 |
settings |
否 | 在默认值之上合并 |
qso_ids |
否 | 立即关联的 QSO,见关联 QSO |
成功时返回 201 Created,带有指向新会话的 Location 响应头,并在 data 中返回创建好的会话。
比赛必须存在于比赛目录中且处于启用状态;未知或已停用的比赛返回 400 validation_error。目录资源列出了可接受的取值。qso_ids 列表会在会话创建之前校验,因此被拒绝的关联列表绝不会留下一个废弃的空会话。
更新会话
curl -X PATCH "https://<WAVELOG_URL>/index.php/api/v2/contest/17" \
-H "Authorization: Bearer wl2_your_token_here" \
-H "Content-Type: application/json" \
-d '{"time_end": "2026-10-18 16:00", "settings": {"serial_per_band": true}}'
只有请求体中出现的字段会改变。settings 会逐键合并到已存储的设置之上——发送 {"serial_per_band": true} 不会改动其他任何设置。既无可编辑字段又无关联列表的请求体会返回 400 validation_error。
关联与取消关联 QSO
URL 空间没有子路径,因此关联关系放在 PATCH 的请求体中传递:
curl -X PATCH "https://<WAVELOG_URL>/index.php/api/v2/contest/17" \
-H "Authorization: Bearer wl2_your_token_here" \
-H "Content-Type: application/json" \
-d '{"link_qso_ids": [48190, 48200], "unlink_qso_ids": [47001]}'
响应是会话对象并附加操作结果——仅当请求中确实包含关联列表时才会出现这些额外字段:
{
"data": {
"id": 17,
"qso_count": 413,
"linked": 2,
"skipped": [],
"unlinked": 1
}
}
| 字段 | 含义 |
|---|---|
linked |
新关联到该会话的 QSO 数量 |
skipped |
已关联到其他会话因而未作改动的 id |
unlinked |
已移除的关联数量 |
关联是幂等的:已关联到本会话的 id 会被静默忽略,因此同步客户端可以重发完整列表而无需特殊处理。关联到其他会话的 id 永远不会被抢占——它会落入 skipped。
关联还会把 QSO 的 CONTEST_ID 字段设为该会话的比赛,取消关联时再次清除它(仅针对确实关联到本会话的 QSO),与高级日志的挂接/解除挂接流程一致。
不属于令牌所有者的 QSO id 会返回 403 forbidden,违规的 id 位于 error.details.qso_ids 中;整个列表会在应用任何改动之前完成校验,因此被拒绝的请求不会改变任何内容。
删除会话
curl -X DELETE "https://<WAVELOG_URL>/index.php/api/v2/contest/17" \
-H "Authorization: Bearer wl2_your_token_here"
返回 204 No Content。默认情况下,已关联的 QSO 保留在日志中,只是失去会话关联——与在 Web 界面中取消勾选复选框相同。
若要彻底拆除整个比赛过程(包括 QSO):
curl -X DELETE "https://<WAVELOG_URL>/index.php/api/v2/contest/17?delete_qsos=true" \
-H "Authorization: Bearer wl2_your_token_here"
警告:
delete_qsos=true还需要qso:delete删除日志中的通联属于 QSO 资源的权限。仅持有
contest:delete的令牌会得到403 insufficient_scope,且details.required_scope = "qso:delete",并且不会删除任何内容。已删除的 QSO 就此消失——无法撤销。
集体台令牌
对于集体台令牌,成员的权限级别会在作用域之上额外生效,与 Web 界面允许的行为一致:
| 操作 | 所需级别 |
|---|---|
| 列出与读取会话 | 任意成员 |
| 创建、更新或删除会话 | 9(集体台干事) |
| 关联与取消关联 QSO | 任意成员——但仅限自己的 QSO |
低于干事级别的成员若尝试编辑会话本身,会得到 403 insufficient_club_permission。会话是集体台共享的基础设施,因此其字段仅限干事修改,而把自己的 QSO 挂接到运行中的会话则不然——这与高级日志采用的分工相同。
错误
| 状态码 | 代码 | 场景 |
|---|---|---|
| 400 | validation_error |
字段缺失或格式错误、比赛未知/未启用、设置无效、更新为空 |
| 403 | forbidden |
该令牌无权访问的 station_id 或 QSO id |
| 403 | insufficient_scope |
缺少 contest:* 作用域,或 delete_qsos=true 时缺少 qso:delete |
| 403 | insufficient_club_permission |
低于干事级别的集体台成员编辑会话 |
| 404 | not_found |
该令牌所有者不存在此会话 |