jetlog 1.2 REST API 使用详解
jetlog 1.2 REST API 全端点实测:Swagger 文档页与 Bearer 令牌认证、20 个路径 25 个操作分域速查、航班增删改查与时长/航距自动计算、CSV/ICS 导出与 MyFlightRadar24/自定义/Flighty 三格式导入、用户管理与权限矩阵,沉淀 IATA 静默丢弃、Note 列丢弃等 6 条排障实录。
jetlog 1.2 REST API 使用详解
目标:把 jetlog v1.2 的 REST API 全部用起来——接口文档页在哪、令牌怎么拿、20 个路径 25 个操作逐域实测(航班增删改查、机场航司检索、统计、地理数据、导入导出、用户管理),并把实测中踩到的坑沉淀成排障实录。全部请求与响应均在 Ubuntu 22.04 服务器(jetlog v1.2,双实例并存)上用 curl 实测通过,实验日期 2026-10-31。
一、环境信息
| 项 | 取值 |
|---|---|
| 操作系统 | Ubuntu 22.04(x86_64) |
| jetlog 版本 | v1.2(官方最新稳定版,API 层为 FastAPI + uvicorn) |
| 主实例 | http://127.0.0.1:8134,管理员 admin / q1w2e3r4(已按部署篇流程改密),另有普通用户 pilot(权限对照用) |
| 副实例 | http://127.0.0.1:8144,全新 v1.2 数据副本,保留默认口令 admin / admin 未改——导入实验在该实例从 0 条数据做起,不污染主实例 |
| 主实例数据 | 管理员名下 5 条航班(id 1–5,2026-02 至 2026-10),普通用户 pilot 名下 1 条(id 6) |
| 客户端工具 | curl 8.x、python3(截取长 JSON 与解码 JWT 用) |
| 实测日期 | 2026-10-31 |
部署方法见 《jetlog 1.2 Docker Compose 部署指南》,环境变量与令牌机制见 《jetlog 1.2 配置参数详解》。jetlog 是前后端同源的架构:Web 界面与 REST API 走同一个端口,前端页面本身就是这组 API 的消费者——所以凡是网页上能做的操作,API 都能做。
二、前置条件
2.1 端点与文档页
- 服务根地址:
http://<实例IP>:3000(或你的映射端口/反代地址),本文以主实例http://127.0.0.1:8134书写; - 全部业务端点挂在
/api前缀下;另有一个无需认证的配置自省端点GET /config; - 接口文档页就是 FastAPI 自带的 Swagger UI:
http://127.0.0.1:8134/docs。实测三个文档入口全部可用:
for p in docs redoc openapi.json; do
curl -s -o /dev/null -w "$p %{http_code}\n" http://127.0.0.1:8134/$p
done
实测回显:
docs 200
redoc 200
openapi.json 200
浏览器打开 /docs 即可看到全部 25 个操作的交互式调试页(Authorize 按钮填令牌、Try it out 直接发请求);/redoc 是同一份 OpenAPI 的阅读版;/openapi.json 是机器可读的接口清单——本文的端点全景(第三章)就是从运行实例的 /openapi.json 实拉的。
2.2 认证方式:OAuth2 密码模式 + Bearer 令牌
jetlog 只有一种 API 认证:先拿用户名密码换 JWT 令牌,之后所有请求带 Authorization: Bearer <token>。AUTH_HEADER 反代头认证是部署层的另一条通道(优先于 Bearer 且有覆盖风险),其行为与风险在 《jetlog 1.2 配置参数详解》5.1 节有专项实测,API 编写方建议保持默认关闭。
💡 技术拓展:Web API 常见的认证方案对比——
方案 凭据形态 典型用法 适用场景 OAuth2 密码模式 + Bearer 短期 JWT(jetlog 默认 7 天) Authorization: Bearer <jwt>前后端分离 Web 应用(jetlog 采用) Basic 认证 用户名:密码 Base64 Authorization: Basic <b64>服务端到服务端简单调用 API Key 长期静态密钥 请求头或查询参数 服务器间集成、无用户语义 会话 Cookie 服务端会话 浏览器自动携带 传统多页应用 jetlog 选密码模式 + JWT:登录端点
POST /api/auth/token接收表单编码的用户名密码(这正是 OAuth2 密码模式的标准形态,Swagger UI 的 Authorize 对话框原生支持);签发的 JWT 自带过期时间、无需服务端存会话,代价是没有吊销机制——作废全部令牌的唯一手段是更换SECRET_KEY(实测见配置篇 5.1 跨实例互拒实验)。没有 API Key 机制,脚本化调用就用「登录换令牌 → 带令牌调接口」的两步法。
2.3 客户端调用范式(两步法)
第一步登录拿令牌,第二步带令牌调业务接口。用 shell 变量串起来:
BT=$(curl -s -X POST -d 'username=admin&password=q1w2e3r4' \
http://127.0.0.1:8134/api/auth/token | sed -E 's/.*"access_token":"([^"]+)".*/\1/')
curl -s -H "Authorization: Bearer $BT" http://127.0.0.1:8134/api/users/me
实测回显(第二步;长回显节选):
{"id":1,"username":"admin","passwordHash":"$argon2id$v=19$m=65536,t=3,p=4$+D0/d4To4GKJlvafxlaSBA$OdXQTVO+neJ+4TTGifm82Ds3a/nhXNaN47vgKFG6HBQ","isAdmin":true,"lastLogin":"2026-10-30T16:46:28","createdOn":"2026-10-30T15:19:10"}
/api/users/me 是最省事的认证自检端点:能返回自己的身份 JSON 就说明令牌有效。注意回显里带 passwordHash(argon2id 哈希)——接口是全量回显用户对象的,集成脚本落日志时注意脱敏。
三、API 总览
3.1 协议与编码约定
- 协议:HTTP/1.1 REST,请求/响应体均为 JSON(登录端点例外,收表单编码);
- 修改与删除的 id 放查询参数,不放路径:
PATCH /api/flights?id=19、DELETE /api/flights?id=19——把 id 写成路径(/api/flights/19)实测返回405 Method Not Allowed(排障实录 8.4); - 成功状态码:新建
201、查询/修改/删除200、导入202(响应体为null); - 错误状态码:
401令牌缺失/无效/口令错误、403权限不足、404资源不存在、422请求体校验失败(Pydantic 校验错误明细); - 两个常用的通用查询参数:
metric(距离单位,true=公里默认 /false=英里)、timezones(新建/修改航班时是否按机场时区换算时长,默认true)。
3.2 端点全景(20 个路径 / 25 个操作,按域分组)
| 域 | 端点(方法) | 说明 |
|---|---|---|
| 认证 | POST /api/auth/token | 表单换 JWT 令牌 |
| 用户 | GET /api/users、POST /api/users、GET /api/users/me、PATCH/DELETE /api/users/{username}、GET /api/users/{username}/details | 用户列表/创建/自检/修改/删除/详情 |
| 航班 | GET /api/flights、POST /api/flights、PATCH /api/flights、DELETE /api/flights | 航班增删改查(id 走查询参数) |
| 航班辅助 | POST /api/flights/many、POST /api/flights/connections、POST /api/flights/airlines_from_callsigns | 批量新建、航段关联计算、按航班号补全航司 |
| 机场 | GET /api/airports?q=、GET /api/airports/{icao} | 模糊检索(限 5 条)、ICAO 精确查询 |
| 航司 | GET /api/airlines?q=、GET /api/airlines/{icao} | 同上 |
| 统计 | GET /api/statistics | 全量统计(支持 metric/start/end/username) |
| 地理 | GET /api/geography/world、GET /api/geography/decorations | 世界地图 GeoJSON、航迹线与坐标点 |
| 导入导出 | POST /api/importing、POST /api/exporting/csv、POST /api/exporting/ical | 三格式 CSV 导入、CSV/ICS 导出 |
| 系统 | GET /config | 配置自省(无需认证) |
四、认证与首次调用
4.1 未认证探测(401 实录)
不带令牌直接访问业务端点:
curl -s -w '\n[%{http_code}]\n' http://127.0.0.1:8134/api/flights
实测回显:
{"detail":"Invalid token"}
[401]
口令错误时同样 401,但 detail 不同:
curl -s -w '\n[%{http_code}]\n' -X POST -d 'username=admin&password=wrong-pass' \
http://127.0.0.1:8134/api/auth/token
实测回显:
{"detail":"Incorrect username or password"}
[401]
两个 401 的 detail 可区分「没带令牌」与「凭据错误」,排障时先看响应体。
4.2 登录换取令牌
curl -s -w '\n[%{http_code}]\n' -X POST -d 'username=admin&password=q1w2e3r4' \
http://127.0.0.1:8134/api/auth/token
实测回显(令牌为 56 字符级 JWT,此处截断):
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc5Mzk4MzU4OH0.bpyjq_kJj2xYlrx1tV3QSdoJCk-hUCI8-Z1uPIzPb8I","token_type":"bearer"}
[200]
请求必须是表单编码(-d 默认即 application/x-www-form-urlencoded),传 JSON 会 422。令牌有效期由 TOKEN_DURATION 控制(默认 7 天,写入 JWT 的 exp 字段,解码实测见配置篇 5.1)。
4.3 普通用户自检
普通用户 pilot 的令牌同样由登录端点签发(下文以 $PT 引用):
curl -s -H "Authorization: Bearer $PT" http://127.0.0.1:8134/api/users/me
实测回显:
{"id":2,"username":"pilot","passwordHash":"$argon2id$v=19$m=65536,t=3,p=4$YwHJ8x5A/3AuMf3yorocEQ$6w6bJZ9MaCwXWZNLXbzcm2BXKCUmpDyeY36dOpopFqk","isAdmin":false,"lastLogin":"2026-10-30T16:48:46","createdOn":"2026-10-30T15:21:23"}
isAdmin:false——第六、七章的权限对照都以这个身份发起。
五、核心端点分域详解
5.1 航班查询:GET /api/flights
查询参数一览(全部可选):id(精确单条)、metric、limit(默认 50)、offset、order(ASC/DESC,默认 DESC)、sort(date/seat/aircraft_side/ticket_class/duration/distance,默认 date)、start/end(日期区间)、origin/destination(ICAO)、username。
基础查询(主实例管理员名下 5 条,按日期倒序):
curl -s -H "Authorization: Bearer $BT" "http://127.0.0.1:8134/api/flights?limit=50" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print([(f['id'],f['date'],f['duration'],f['distance']) for f in d])"
实测回显:
[(1, '2026-10-12', 145, 1100), (2, '2026-09-03', 155, 1175), (3, '2026-08-15', 130, 695), (4, '2026-07-20', 215, 2517), (5, '2026-05-02', 185, 1838)]
排序换轴:sort=duration&order=ASC&limit=2:
[(3, '2026-08-15', 130), (1, '2026-10-12', 145)]
组合过滤:start=2026-05-01&end=2026-06-30&origin=ZGOW:
[(5, '2026-05-02', 'ZGOW', 'ZBAA')]
单位换算:metric=false 时距离从公里换算成英里,id=1 实测 1100 → 684(换算系数 0.6213711922 后四舍五入)。
单条详情(id=19 为临时新建的航班,响应体是完整的 FlightModel):
{"id":19,"username":"admin","date":"2026-03-18","origin":{"icao":"ZBAD","iata":"PKX","type":"large_airport","name":"Beijing Daxing International Airport","municipality":"Beijing","region":"Beijing Municipality","country":"China","continent":"Asia","latitude":39.509945,"longitude":116.41092,"timezone":"Asia/Shanghai"},"destination":{"icao":"ZLLL","iata":"LHW","type":"large_airport","name":"Lanzhou Zhongchuan International Airport","municipality":"Lanzhou (Yongdeng)","region":"Gansu Province","country":"China","continent":"Asia","latitude":36.515202,"longitude":103.620003,"timezone":"Asia/Shanghai"},"departureTime":"09:30","arrivalTime":"11:05","arrivalDate":null,"seat":"window","aircraftSide":null,"ticketClass":"economy","purpose":"leisure","duration":95,"distance":1168,"airplane":null,"airline":null,"tailNumber":null,"flightNumber":"HU7245","notes":"D5实测新增","connection":null}
注意机场字段是完整对象(ICAO/IATA/坐标/时区),不是字符串代码——前端地图与统计都直接消费这些对象。查不存在的 id 返回 404 与 {"detail":"Flight not found."}(实测 id=999)。
5.2 航班新增:POST /api/flights
必填三个字段:date、origin、destination(ICAO 字符串);其余(座位、舱位、机型、备注等 14 个可选字段)见 5.1 响应体结构。时长与航距不用手工算:给了起降时间就按机场时区自动换算时长,给了机场就按 Haversine 公式自动算航距。实测(ZBAD 大兴 → ZLLL 兰州,09:30 起飞 11:05 到达):
curl -s -w ' [%{http_code}]\n' -X POST -H "Authorization: Bearer $BT" -H 'Content-Type: application/json' \
-d '{"date":"2026-03-18","origin":"ZBAD","destination":"ZLLL","departure_time":"09:30","arrival_time":"11:05","seat":"window","ticket_class":"economy","purpose":"leisure","flight_number":"HU7245","notes":"D5实测新增"}' \
http://127.0.0.1:8134/api/flights
实测回显:
19
[201]
响应体只有新 id。随后的 GET /api/flights?id=19 实测(见 5.1 完整 JSON):duration=95(分钟,同 时区下恰为 95 分钟差)、distance=1168(公里,Haversine 球面距离)——两个值请求体里都没传,全是服务端算的。跨时区航班会先转 UTC 再相减(timezones=false 可关掉换算,按字面时间差计算)。
校验失败形态(实测两种):
# 缺 origin/destination 字段
curl -s -X POST -H "Authorization: Bearer $BT" -H 'Content-Type: application/json' \
-d '{"date":"2026-03-18"}' http://127.0.0.1:8134/api/flights
实测回显(422,Pydantic 明细,节选):
{"detail":[{"type":"missing","loc":["body","origin"],"msg":"Field required","input":{"date":"2026-03-18"},"url":"https://errors.pydantic.dev/2.3/v/missing"},{"type":"missing","loc":["body","destination"],"msg":"Field required",...}]}
机场码写错(空串或非 ICAO)也是 422,但 detail 换成字段校验器的话术:
{"detail":[{"type":"value_error","loc":["body","origin","icao"],"msg":"Value error, must have valid ICAO code, got ''",...}]}
给谁建航班是个易错点:请求体里的 username 字段语义是「替别人建」。普通用户建自己的航班必须显式带 username 等于自己——不带实测 403(排障实录 8.3):
{"detail":"Only admins can add flights for other users"}
[403]
普通用户带上自己的 username 后实测 201(id 23,验证后已删)。管理员替他人建:请求体多带 "username":"pilot" 实测 201;反过来普通用户试图替 admin 建则 403。
5.3 航班修改与删除:PATCH / DELETE /api/flights
PATCH 部分更新——id 走查询参数,请求体只传要改的字段(实测改备注与座位):
curl -s -w ' [%{http_code}]\n' -X PATCH -H "Authorization: Bearer $BT" -H 'Content-Type: application/json' \
-d '{"notes":"D5实测改备注","seat":"aisle"}' "http://127.0.0.1:8134/api/flights?id=19"
实测回显:
19
[200]
回读确认 notes=D5实测改备注、seat=aisle;且修改机场或起降时间时,航距/时长会自动重算(不传 distance/duration 的前提下)。空请求体 {} 实测同样返回 19 [200]——等于无操作,不报错。
DELETE——同样查询参数传 id,响应体是被删的 id:
curl -s -w ' [%{http_code}]\n' -X DELETE -H "Authorization: Bearer $BT" "http://127.0.0.1:8134/api/flights?id=19"
实测回显:
19
[200]
删除后 GET /api/flights?id=19 实测 404 {"detail":"Flight not found."}——闭环。权限上,非管理员修改/删除他人航班返回 403 {"detail":"Only admins can modify other users' flights"}(pilot 身份实测)。
5.4 批量新建与航段工具
POST /api/flights/many——请求体是航班数组,返回「给请求者自己建的最后一个航班 id」。实测一个反直觉行为(两条都不带 username):
curl -s -w ' [%{http_code}]\n' -X POST -H "Authorization: Bearer $BT" -H 'Content-Type: application/json' \
-d '[{"date":"2026-03-19","origin":"ZBAA","destination":"ZSPD"},{"date":"2026-03-20","origin":"ZSPD","destination":"ZBAA","departure_time":"12:00","arrival_time":"14:25"}]' \
http://127.0.0.1:8134/api/flights/many
实测回显:
-1
[201]
返回 -1 但航班实际建成了(区间查询实测 [('2026-03-20', 145, 1100), ('2026-03-19', None, 1100)] 两条都在)。原因:返回值只统计请求体里显式写了 username 且等于请求者本人的条目——批量建自己的航班不带 username 时,id 统计落空即 -1。排障实录 8.6 有详细解读。验证后两条已删除。
POST /api/flights/connections——把「次日~后日内原路折返」的航班对标记为航段(connection),统计端点据此去重:
curl -s -w ' [%{http_code}]\n' -X POST -H "Authorization: Bearer $BT" \
http://127.0.0.1:8134/api/flights/connections
实测回显:
{"amountSkipped":0,"amountUpdated":0}
[200]
POST /api/flights/airlines_from_callsigns——对所有「有航班号但没航司」的航班调公共航班数据库(adsbdb)补全航司。该端点受 ENABLE_EXTERNAL_APIS 门控,关闭时 400(原文见配置篇 5.3)。开启实例实测(库里有一条无航司的 MU5301):
{"amountSkipped":1,"amountUpdated":1}
[200]
补全结果直接写库,随后的 CSV 导出里该行 airline 列从空变为 CES——闭环确认。
5.5 机场与航司检索
四个只读端点,给前端输入框做自动补全,脚本里可以用来校验机场码/拿坐标。
模糊检索(q 必填,最多返回 5 条,按匹配度排序):
curl -s -H "Authorization: Bearer $BT" "http://127.0.0.1:8134/api/airports?q=Shanghai" | head -c 300
实测回显(节选):
[{"icao":"CN-0169","iata":"","type":"medium_airport","name":"Shanghai Dachang Air Base","municipality":"Shanghai (Baoshan)",...
命中 3 条:大场机场、浦东(ZSPD/PVG)、虹桥(ZSSS/SHA)——城市名、机场名、IATA/ICAO 码都能当关键词。
ICAO 精确查询(大小写不敏感,实测小写 ces 照样命中):
curl -s -H "Authorization: Bearer $BT" http://127.0.0.1:8134/api/airports/ZBAA
curl -s -H "Authorization: Bearer $BT" http://127.0.0.1:8134/api/airlines/ces
实测回显:
{"icao":"ZBAA","iata":"PEK","type":"large_airport","name":"Beijing Capital International Airport","municipality":"Beijing","region":"Beijing Municipality","country":"China","continent":"Asia","latitude":40.080101013183594,"longitude":116.58499908447266,"timezone":"Asia/Shanghai"}
{"icao":"CES","iata":"MU","name":"China Eastern Airlines"}
查不存在的码返回 404:{"detail":"No airport with ICAO 'XXXX' found"}(航司同构 No airline with ICAO '...' found)。航司模糊检索实测 q=Eastern 命中 5 条(Eastern Airways、Eastern Airlines、Eastern Air Express、China Eastern Airlines、Eastern Australia Airlines)。
5.6 统计端点:GET /api/statistics
一次返回全部聚合指标(Top5 机场/国家/航司、座位与舱位频次),支持 metric、start/end、username 参数。主实例全量实测回显:
{"totalFlights":5,"totalDuration":830,"totalDistance":7325,"totalUniqueAirports":6,"daysRange":163,"visitedCountries":1,"mostVisitedAirports":{"PEK - Beijing/China":3,"SYX - Sanya (Tianya)/China":2,"CAN - Guangzhou (Huadu)/China":2,"SHA - Shanghai (Minhang)/China":1,"PVG - Shanghai (Pudong)/China":1},"mostCommonCountries":{"China":10},"seatFrequency":{"window":3,"aisle":2},"ticketClassFrequency":{"economy":2,"business":2,"economy+":1},"mostCommonAirlines":{"China Eastern Airlines":1,"Air China":1}}
字段解读:totalDuration 单位分钟、totalDistance 公里(metric=false 时换算英里,实测 7325 → 4552);daysRange 是首末航班间隔天数(163 天);visitedCountries 按「非航段的目的地 + 非被接续的出发地」去重统计。区间过滤实测 start=2026-01-01&end=2026-06-30:totalFlights=1、totalDistance=1838、totalDuration=185——只有 id=5 那条 5 月航班入选,边界行为符合预期。
5.7 地理数据:world 与 decorations
GET /api/geography/world——整份世界地图 GeoJSON(约 1.7MB),给前端画图用;加 visited=true 会给每个国家 Feature 注入 visited 布尔字段。主实例实测(1 个到访国家):
curl -s -H "Authorization: Bearer $BT" "http://127.0.0.1:8134/api/geography/world?visited=true" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('visited countries:', sum(1 for f in d['features'] if f['properties'].get('visited')))"
实测回显:
visited countries: 1
GET /api/geography/decorations——返回航迹线与机场坐标点(带重复频次),实测回显节选:
[[{"first":{"latitude":40.080101013183594,"longitude":116.58499908447266,"frequency":3},"second":{"latitude":31.1434,"longitude":121.805,"frequency":1},"frequency":1},...
结构是二元组 [航迹线数组, 坐标点数组],每条线/点带 frequency 供地图加粗;可传 flight_id 只取单条航班的轨迹。
5.8 导入:POST /api/importing
表单上传(multipart,文件字段名 file),查询参数 csv_type 三选一:myflightradar24、flighty、custom。成功一律 202 + 响应体 null。导入实测在副实例(8144,0 条数据起步)进行,全程不污染主实例。
MyFlightRadar24 格式——19 列表头严格校验,第一行列名逐一比对:Date, Flight number, From, To, Dep time, Arr time, Duration, Airline, Aircraft, Registration, Seat number, Seat type, Flight class, Flight reason, Note, Dep_id, Arr_id, Airline_id, Aircraft_id。列名不符实测 400:
{"detail":"Invalid MyFlightRadar24 CSV: Expected column 'Date', got 'Wrong'"}
[400]
构造合规文件实测(注意 From/To 列括号里必须放 ICAO,见排障实录 8.1):
printf 'Date,Flight number,From,To,Dep time,Arr time,Duration,Airline,Aircraft,Registration,Seat number,Seat type,Flight class,Flight reason,Note,Dep_id,Arr_id,Airline_id,Aircraft_id\n2026-04-18,MU5137,Shanghai (ZSPD),Beijing (ZBAA),08:00,10:30,02:30:00,China Eastern Airlines (CES),Airbus A330-300,B-6099,32K,1,2,1,D5导入实测,,,,\n2026-04-20,CA1832,Beijing (ZBAA),Shanghai (ZSSS),11:00,13:15,02:15:00,Air China (CCA),Boeing 737-89L,B-5757,15C,3,1,2,,,,,\n' > mfr_test.csv
curl -s -w ' [%{http_code}]\n' -X POST -H "Authorization: Bearer $NT" \
-F "file=@mfr_test.csv" "http://127.0.0.1:8144/api/importing?csv_type=myflightradar24"
实测回显:
null
[202]
回查数据库,两条全部入库且映射正确(Seat type 1→window、3→aisle;Flight class 1→economy、2→business;Flight reason 1→leisure、2→business;Duration 02:30:00→150 分钟;航距自动算出 1100/1076 公里;Airline 括号里的 ICAO 自动关联航司表)。但 Note 列的内容被静默丢弃(两行的 notes 回查均为空)——MFR24 解析器不消费该列,见排障实录 8.2。
custom 格式——列名取自 FlightModel 字段名,列可以任意排列、任意子集,按表头名对号入座。乱序列实测(notes 放第一列):
printf 'notes,date,destination,origin,flight_number\n自定义导入备注,2026-06-11,ZGOW,ZBAA,KN5957\n' > custom_ok.csv
curl -s -w ' [%{http_code}]\n' -X POST -H "Authorization: Bearer $NT" \
-F "file=@custom_ok.csv" "http://127.0.0.1:8144/api/importing?csv_type=custom"
实测回显:
null
[202]
回查 id=3:notes=自定义导入备注、flightNumber=KN5957、distance=1838(自动算)、duration=None(没给起降时间,不猜)——列序打乱、字段子集都正常。错误形态实测两种:列名非法 400 {"detail":"Invalid column name 'wrongcol'"};列名重复 400 {"detail":"Duplicate column name 'date'"}。另外 username 列仅管理员可用(非管理员实测 403)、connection 列禁止出现在导入文件(400)。
Flighty 格式——33 列固定表头,不符直接 400(实测把 custom 文件误投 flighty 类型):
{"detail":"Flighty CSV columns do not match expected structure"}
[400]
该格式会自动跳过取消航班与无实际到达时间的航班,并把座位号、航站楼、登机口合并进备注。
5.9 导出:csv 与 ical
两个端点都是 POST(易误写成 GET)且无需参数,导出当前用户自己的全部航班。实测下载头(主实例 CSV):
curl -s -D - -o export.csv -X POST -H "Authorization: Bearer $BT" http://127.0.0.1:8134/api/exporting/csv
实测回显(响应头节选):
HTTP/1.1 200 OK
server: uvicorn
content-type: text/csv; charset=utf-8
content-disposition: attachment; filename="jetlog.csv"
CSV 是 17 列固定表头(date,origin,destination,departure_time,arrival_time,arrival_date,seat,aircraft_side,ticket_class,purpose,duration,distance,airplane,airline,tail_number,flight_number,notes),日期倒序;origin/destination 列回落为 ICAO 字符串、空值为空串——即导出文件可直接当 custom 格式导回(列名完全一致,闭环)。ICS 导出同理(filename="jetlog.ics",content-type text/plain):每条航班一个 VEVENT,有起降时间时输出 DTSTART/DTEND 精确到分钟,没有时输出全日事件格式——实测(无起降时间的 id=3):
BEGIN:VEVENT
SUMMARY:Flight from Beijing to Jieyang (Rongcheng)
DESCRIPTION:Origin: PEK - Beijing/China\nDestination: SWA - Jieyang (Rongcheng)/China\n\nNotes: 自定义导入备注
DTSTART;VALUE=DATE:20260611
DTEND;VALUE=DATE:20260611
END:VEVENT
六、用户管理:/api/users 全生命周期
用户端点只认 JWT 身份,写操作基本是管理员专属。完整生命周期实测(创建 → 列表 → 详情 → 提权 → 删除 → 验尸):
curl -s -w ' [%{http_code}]\n' -X POST -H "Authorization: Bearer $BT" -H 'Content-Type: application/json' \
-d '{"username":"copilot","password":"q1w2e3r4"}' http://127.0.0.1:8134/api/users
实测回显(创建成功,响应体为 null):
null
[201]
curl -s -w ' [%{http_code}]\n' -H "Authorization: Bearer $BT" http://127.0.0.1:8134/api/users
curl -s -w '\n[%{http_code}]\n' -H "Authorization: Bearer $BT" http://127.0.0.1:8134/api/users/copilot/details
实测回显:
["admin","copilot","pilot"]
[200]
{"id":5,"username":"copilot","passwordHash":"$argon2id$v=19$m=65536,t=3,p=4$Dr5A77eLo4Vn/PniIsv3Hg$LXT5kNlbBLOPNvtj+Intfk8ShgozcQ9QXBsCFnEdtQ0","isAdmin":false,"lastLogin":null,"createdOn":"2026-10-31T00:59:01"}
[200]
GET /api/users 只返回用户名数组(无分页);details 才是完整对象。PATCH 支持改用户名、密码(明文传入、服务端 argon2id 落库)、管理员标志,实测提权:
curl -s -w ' [%{http_code}]\n' -X PATCH -H "Authorization: Bearer $BT" -H 'Content-Type: application/json' \
-d '{"is_admin":true}' http://127.0.0.1:8134/api/users/copilot
实测回显:
null
[200]
回查 details 实测 isAdmin=True(改回后再删,避免留测试账号)。两个防呆实测:不能删自己(400 {"detail":"You cannot delete your own user"});删除后会连带的该用户全部航班一起删。删除 copilot 后查详情实测 404 {"detail":"User 'copilot' not found"}。普通用户的边界实测:pilot 调 POST /api/users 返回 403 {"detail":"Only admins can create new users"};pilot 查 admin 的 details 返回 403 {"detail":"Only admins can get other users' details"};但改自己的密码(PATCH /api/users/pilot)是允许的——「自己改自己」不受管理员限制。
七、权限模型与安全观察
| 操作 | 管理员 | 普通用户 |
|---|---|---|
登录、GET /api/users/me、查自己的航班/统计 | ✓ | ✓ |
| 创建/修改/删除自己的航班 | ✓ | ✓(需带自己的 username) |
| 替他人创建/修改/删除航班 | ✓ | ✗ 403 |
GET /api/users 用户名清单 | ✓ | ✓(实测未限制) |
GET /api/users/{u}/details | 任意 | 仅自己 |
| 创建/删除用户、提权 | ✓ | ✗ 403 |
| 修改自己密码、自己用户名 | ✓ | ✓ |
三条实测出来的安全观察,集成前值得知道:
- 跨用户读是开放的:
GET /api/flights?id=<他人航班>、GET /api/flights?username=<他人>、GET /api/statistics?username=<他人>实测普通用户身份全部200(pilot 读到 admin 全部 5 条)——写有权限墙、读没有。多用户实例上把它当「全家共享的旅行账本」用没问题,若有隐私隔离预期就要靠反向代理层补一道。 - 用户接口全量回显
passwordHash(argon2id):/api/users/me与details都带哈希字段,日志与监控采集时注意脱敏。 - 令牌无吊销:JWT 到期前始终有效,泄露应急手段是更换
SECRET_KEY(全部令牌立即失效,实测见配置篇 5.1)。
八、排障实录
Q1:导入返回 202,但一条都没进去?
MFR24 格式的 From/To 列括号里放的是 IATA 码(如 Shanghai (PVG))时,行级解析全部失败但接口照样 202——逐行失败只写服务端日志,客户端拿到的永远是 null [202]。实测换 ICAO(Shanghai (ZSPD))后同样文件 2 条全部入库。排查口诀:202 后必须回查 GET /api/flights 数条数,别信状态码。
Q2:MFR24 导入的备注丢了?
解析器不消费 Note 列(19 列里唯一被读后丢弃的字段),实测两行带备注的 CSV 入库后 notes 均为空。要保备注,把 MFR24 文件预处理成 custom 格式(列名 notes),或导入后用 PATCH /api/flights 补。
Q3:普通用户建自己的航班报 403 Only admins can add flights for other users?
请求体没带 username。服务端把「不带 username」当作「要给别人建」处理(None != 'pilot' 触发权限检查),实测带上 "username":"pilot" 即 201。这是最容易踩的字段语义坑。
Q4:PATCH/DELETE 报 405 Method Not Allowed?
id 写进路径了(PATCH /api/flights/19)。这组端点的 id 只认查询参数(?id=19),路径形态没有对应路由,实测返回 {"detail":"Method Not Allowed"} [405]。
Q5:缺字段报 422 而不是文档里说的 404?
源码里有「date/origin/destination 不全则 404」的分支,但 API 层的请求体模型先行校验——缺字段/字段为 null/机场码非法都在 Pydantic 层就被 422 拦下,实测未触达 404 分支。排障时按 422 的 detail 明细修请求体即可。
Q6:POST /api/flights/many 返回 -1,航班到底建没建?
建了。返回值是「请求体里显式写了 username 且等于请求者本人」的最后一个 id,全部不带 username 时统计落空返回 -1,但航斑实际逐条入库(实测 2 条不带 username 的数组返回 -1、区间查询两条俱在)。同样以回查为准。
九、参考链接
- 官方 GitHub 仓库:https://github.com/pbogre/jetlog(API 实现在
server/routers/,本文端点行为与之一一核对) - 运行实例自带的接口文档:
http://<实例IP>:3000/docs(Swagger UI)与/redoc - 本专栏相关篇目:《jetlog 介绍与选型指南》(认知与选型)、《jetlog 1.2 Docker Compose 部署指南》(部署与改密实测)、《jetlog 1.2 配置参数详解》(环境变量逐项实测)
本文全部请求与回显实测于 2026-10-31,jetlog v1.2;API 行为以运行实例的
/openapi.json与官方源码为准,升级后请复核差异。