jetlog 1.2 REST API 使用详解

jetlog 1.2 REST API 全端点实测:Swagger 文档页与 Bearer 令牌认证、20 个路径 25 个操作分域速查、航班增删改查与时长/航距自动计算、CSV/ICS 导出与 MyFlightRadar24/自定义/Flighty 三格式导入、用户管理与权限矩阵,沉淀 IATA 静默丢弃、Note 列丢弃等 6 条排障实录。

jetlogjetlogAPIREST API2026/10/31

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 认证用户名:密码 Base64Authorization: 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
修改自己密码、自己用户名✓✓

三条实测出来的安全观察,集成前值得知道:

  1. 跨用户读是开放的:GET /api/flights?id=<他人航班>、GET /api/flights?username=<他人>、GET /api/statistics?username=<他人> 实测普通用户身份全部 200(pilot 读到 admin 全部 5 条)——写有权限墙、读没有。多用户实例上把它当「全家共享的旅行账本」用没问题,若有隐私隔离预期就要靠反向代理层补一道。
  2. 用户接口全量回显 passwordHash(argon2id):/api/users/me 与 details 都带哈希字段,日志与监控采集时注意脱敏。
  3. 令牌无吊销: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、区间查询两条俱在)。同样以回查为准。

九、参考链接

本文全部请求与回显实测于 2026-10-31,jetlog v1.2;API 行为以运行实例的 /openapi.json 与官方源码为准,升级后请复核差异。