Alf.io 2.0-M5-2609 API 与集成指南
讲解 Alf.io 2.0-M5-2609 的 REST API 体系(v2 公开 API、v1 管理 API、管理后台会话 API),给出 Bearer 系统密钥与 apikey 客户端两类认证的实测调用、CSRF 机制取证、错误码对照与从创建活动到公开发布的端到端集成实战,全部请求与响应均实测于 Ubuntu 22.04 部署实例。
Alf.io 2.0-M5-2609 API 与集成指南
目标:面向需要把 Alf.io 2.0-M5-2609 接入自有系统(活动同步、订单创建、组织管理、数据看板)的集成开发者,讲清这套票务系统的 REST API 全貌——两代 API 的分工、两类密钥的认证方式、公开域与管理域的端点用法、CSRF 防护对脚本化写入的真实影响,并以「创建活动 → 发布 → 公开可见」的端到端串联收尾。全部请求与响应均在 《Ubuntu 22.04 安装 Alf.io 2.0-M5-2609 指南》 部署的 Ubuntu 22.04 实例上实测通过(实例地址
https://alfio.example.local,自签名 TLS)。
一、环境信息
本指南所有命令与回显均来自以下实测环境,读者复现时请对照自己的实例参数(域名、端口、账号):
- 操作系统:Ubuntu 22.04(
/opt/alfio/安装目录,systemd 服务alfio托管) - Alf.io 版本:2.0-M5-2609(应用文件
alfio-2.0-M5-2609-boot.jar,Spring Boot 3.x 内嵌 Tomcat/Jetty) - 访问入口:
https://alfio.example.local(8443,自签名 TLS;alfio.base-url=https://alfio.example.local配置于application.properties) - 管理后台账号:
admin(首次启动自动生成的随机密码,本实例实测为NNtHfOzVERRyVRB6j) - 数据库:PostgreSQL(本机 5432,库名
alfio,用户alfio,测试口令q1w2e3r4,见安装篇 4.4 节) - 实例内已有演示数据:组织
Demo Organization(id=1,含活动democonf2026)、组织Alfio Docs Org(id=2,含活动d5apidemo) - 已配置的系统级 API 密钥:
SYSTEM_API_KEY(配置表中的 75 字符随机串,获取方法见第四章) - 已创建的 API 客户端账号:归属组织 2,用户名即 API 密钥
bc333b51-b8c1-4fc9-8d42-d5ade41af9de(创建方法见第七章) - 客户端工具:
curl(HTTP 调用)、psql(读取系统密钥用,可省略)
💡 说明:
democonf2026、d5apidemo等演示数据是在本文实测过程中逐步创建的,读者在全新实例上跟随操作会得到自己创建的同构数据,端点行为一致。
二、前置条件
本章确认开始集成前需要就绪的三类条件:一个可访问的 Alf.io 实例、两类可用的凭据、一套客户端工具。
2.1 实例就绪
按 《Ubuntu 22.04 安装 Alf.io 2.0-M5-2609 指南》 完成安装后,先做两项健康检查:
systemctl is-active alfio
实测输出:
active
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost:8443/
实测输出:
302
服务 active 且首页返回 302(重定向到活动列表页),说明实例可访问。-k 参数用于忽略自签名证书告警,正式环境换用受信证书后可去掉。
2.2 两类凭据的就绪状态
Alf.io 的 API 认证凭据分「系统级」与「客户端级」两种,本章先确认其在实例中的存在性,创建/获取方法在对应章节展开:
- 系统 API 密钥(SYSTEM_API_KEY):配置表中的一个配置键,用管理后台(系统配置 → API Key)或直接查库设置。本文实例已配置,验证:
PGPASSWORD=q1w2e3r4 psql -h localhost -U alfio -d alfio -t -A -c "select c_key, length(c_value) from configuration where c_key='SYSTEM_API_KEY'"
实测输出:
SYSTEM_API_KEY 75
返回键名与值长度 75,说明密钥已设置。
- API 客户端账号(API_KEY 类型用户):在管理后台「用户管理」中创建、归属某一组织,用户名即 API 密钥。本文实例已有一个归属组织 2 的客户端(密钥
bc333b51-b8c1-4fc9-8d42-d5ade41af9de),创建步骤与实测回显见第七章 7.3 节。
2.3 客户端工具
curl:本指南全部 HTTP 调用工具;psql:仅在演示「从数据库读取系统密钥」时使用,生产集成中系统密钥应由管理后台配置后经安全渠道分发给集成方,不需要每次查库;- 任意 JSON 查看工具(本文用
python3 -m json.tool或直接原样输出)。
三、API 总览
Alf.io 2.0-M5-2609 对外暴露三组 HTTP API,按「调用者身份 + 认证方式」划分。先给全景对照表,再逐组说明:
| API 组 | 路径前缀 | 认证方式 | CSRF 要求 | 典型用途 | 实测结论 |
|---|---|---|---|---|---|
| 公开 API v2 | /api/v2/public/ | 无需认证 | 写操作要求会话 CSRF 令牌 | 活动查询、票种查询、日历订阅、购票流程 | GET 全部可用;POST 对纯脚本客户端被 CSRF 拦截(见 5.5) |
| 管理 API v1 | /api/v1/admin/ | Authorization: Bearer <系统密钥>(系统域)或 Authorization: apikey <客户端密钥>(组织域) | 无(令牌认证链已关闭 CSRF) | 服务端对服务端集成:建组织、建活动、查统计、拉名单 | 本文主推的集成通道,全流程实测可用 |
| 管理后台内部 API | /admin/api/ | 管理会话 Cookie + CSRF 令牌,或 apikey 客户端 | 会话调用必须带 X-CSRF-TOKEN | 管理后台页面自身的 XHR;脚本可复刻会话流程 | 会话式调用实测可用(见第七章) |
关键差异与选型:服务端集成(无人参与)选 v1 管理 API + 密钥认证,它不依赖会话、不受 CSRF 干扰;浏览器端或需要走购票流程的场景用 v2 公开 API,其写操作按浏览器会话方式工作;/admin/api/ 是后台页面自用的 API,脚本集成仅在需要个别未在 v1 开放的运维动作(如活动发布状态切换)时复刻会话流程使用。
💡 技术拓展:Web 应用的 API 认证方案不止本文用到的「静态密钥」。常见方案对比——
方案 原理/特点 适用场景 静态 API Key 请求头携带预共享密钥,服务端查库比对;实现最简单 服务端对服务端、内部集成(本文 Alf.io 的 apikey/Bearer 即此类) Bearer Token / JWT 携带签名令牌,可带过期时间与 claims,无状态校验 开放平台、移动端 OAuth2 / OIDC 授权码/客户端凭据流,支持三方授权与细粒度 scope 平台化开放生态 会话 Cookie + CSRF 登录后以 Cookie 维持会话,写操作需回传 CSRF 令牌 浏览器应用(本文 /admin/api 会话式调用即此类) Alf.io 2.0-M5-2609 的选择:管理域用静态密钥(apikey 客户端 + Bearer 系统密钥),管理后台与购票流程用会话 Cookie + CSRF。本指南服务端集成采用密钥认证,理由是无需维护会话生命周期且天然免疫 CSRF。
另有一项需要提前说明的实测事实:本版本未内置 Swagger/OpenAPI 文档端点。对常见文档端点逐一探测:
for p in /v3/api-docs /swagger-ui/index.html /swagger-ui.html; do
printf '%s -> ' "$p"; curl -sk -o /dev/null -w '%{http_code}\n' "https://localhost:8443$p"
done
实测输出:
/v3/api-docs -> 404
/swagger-ui/index.html -> 404
/swagger-ui.html -> 404
三个端点均 404(应用构建中也未包含 springdoc 依赖)。因此本指南的端点清单以「官方文档 + 应用内置控制器映射 + 逐端点实测」三方对照给出,未实测的端点会明确标注。
四、认证与首次调用
本章完成两类密钥的首次真实调用:先用系统密钥调通「系统域」端点(跨组织视角),再用 apikey 客户端调通「组织域」端点(单组织视角),并实测两者的权限边界。
4.1 系统密钥(Bearer)调用系统域端点
系统密钥用于 /api/v1/admin/system/** 域。先在实例上把密钥读入环境变量(避免在命令历史中留下明文),再发起首次调用:
KEY=$(PGPASSWORD=q1w2e3r4 psql -h localhost -U alfio -d alfio -t -A -c "select c_value from configuration where c_key='SYSTEM_API_KEY'")
curl -sk -w '\nhttp=%{http_code}\n' -H "Authorization: Bearer $KEY" https://localhost:8443/api/v1/admin/system/organization/list
实测输出:
[{"id":2,"name":"Alfio Docs Org","email":"info@example.org","description":"Created for API doc verification","externalId":null,"slug":null},{"id":1,"name":"Demo Organization","email":"info@example.org","description":"Demo org for API test","externalId":null,"slug":null}]
http=200
返回组织列表(两条演示数据)且状态码 200,Bearer 认证生效。
紧接着做无认证对照,确认该端点确实受保护:
curl -sk -w '\nhttp=%{http_code}\n' https://localhost:8443/api/v1/admin/system/organization/list
实测输出:
{"timestamp":"2026-10-23T19:21:50.591+00:00","status":403,"error":"Forbidden","path":"/api/v1/admin/system/organization/list"}
http=403
无凭据时返回 403 Forbidden,错误体为统一 JSON 结构(timestamp/status/error/path 四键,详见第八章)。
系统域端点还能创建组织(实测):
curl -sk -w '\nhttp=%{http_code}\n' -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-X POST https://localhost:8443/api/v1/admin/system/organization/create \
-d '{"name":"Alfio Docs Org","email":"info@example.org","description":"Created for API doc verification"}'
实测输出:
{"id":2,"name":"Alfio Docs Org","email":"info@example.org","description":"Created for API doc verification","externalId":null,"slug":null}
http=200
POST 成功返回新组织对象(id=2)。注意系统域的 POST 无需 CSRF 令牌——令牌认证链默认关闭了 CSRF 校验,这正是密钥认证适合脚本集成的核心原因。
4.2 客户端密钥(apikey 头)调用组织域端点
API 客户端账号的密钥通过 Authorization: apikey <密钥> 头传递,作用于 /api/v1/admin/** 的组织域端点。用现有客户端查活动列表:
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' \
https://localhost:8443/api/v1/admin/event/list
实测输出(节选,实测首尾):
[{"ticketCategories":[{"description":{"en":"<p>Standard admission</p>"},"tokenStatus":null,"metadata":{...},"name":"Standard","id":3,"currencyCode":"EUR","status":"ACTIVE","srcPriceCts":5000,"price":50.00,"eventId":3,...},"description":{"en":"<p>Event created by the API guide probe</p>"},"location":"Lugano, Switzerland","id":3,"timeZone":"Europe/Zurich","displayName":"API Doc Live Demo","currency":"EUR","format":"IN_PERSON","title":{"en":"API Doc Live Demo"},"status":"PUBLIC","shortName":"d5apidemo","organizationId":2,...}]
http=200
返回 200 与活动数组。关键观察:该客户端只看到组织 2 的 d5apidemo,看不到组织 1 的 democonf2026——apikey 客户端的可见范围被隔离在其所属组织内。而 4.1 节的系统密钥能列出全部组织,这是两类密钥最核心的权限差异。
再实测客户端的单活动统计端点:
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' \
https://localhost:8443/api/v1/admin/event/d5apidemo/stats
实测输出(节选,实测首尾):
{"ticketCategories":[{"description":{"en":"<p>Standard admission</p>"},"name":"Standard","id":3,"price":50.00,"eventId":3,"maxTickets":100,"notSoldTickets":100,"soldTickets":0,"pendingTickets":0,...}],"description":{"en":"<p>Event created by the API guide probe</p>"},"grossIncome":0,"location":"Lugano, Switzerland","id":3,"title":{"en":"API Doc Live Demo"},"status":"PUBLIC","shortName":"d5apidemo",...}
http=200
4.3 权限边界实测:客户端密钥碰系统域
用组织级客户端密钥访问系统域端点,验证权限隔离:
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' \
https://localhost:8443/api/v1/admin/system/organization/list
实测输出:
{"timestamp":"2026-10-23T14:10:16.758+00:00","status":403,"error":"Forbidden","path":"/api/v1/admin/system/organization/list"}
http=403
403 拒绝。集成时的结论:系统密钥管组织(跨组织),客户端密钥管活动(单组织),按最小权限原则给集成方分发。
五、公开 API 域详解(/api/v2/public)
公开 API 面向购票者与展示端(官网活动页、日历订阅、购票流程),读取无需任何认证。本章逐端点实测 GET 能力,并对 POST 端点做一次完整的 CSRF 行为取证。
5.1 活动列表:GET /api/v2/public/events
该端点返回当前所有已公开发布的活动摘要:
curl -sk -w '\nhttp=%{http_code}\n' https://localhost:8443/api/v2/public/events
实测输出(节选,实测首尾,保留第一个活动的完整结构):
[{"shortName":"democonf2026","fileBlobId":"2ba16cee87145693eec4063f00c04dea373b02ae9b279f3419b22eab9682ae81","title":{"en":"DemoConf 2026"},"format":"IN_PERSON","location":"Lugano, Switzerland","timeZone":"Europe/Zurich","datesWithOffset":{"startDateTime":1796112000000,"startTimeZoneOffset":3600,"endDateTime":1796230800000,"endTimeZoneOffset":3600},"sameDay":false,"formattedBeginDate":{"en":"Tue 01 Dec 2026","fr":"mardi 01 décembre 2026","it":"mar 01 dic 2026","es":"martes 01 diciembre 2026","de":"Dienstag, 01. Dezember 2026"},"formattedBeginTime":{"en":"09:00"},"formattedEndDate":{"en":"Wed 02 Dec 2026"},"formattedEndTime":{"en":"18:00"},"contentLanguages":[{"locale":"en","displayLanguage":"English"}]},{"shortName":"d5apidemo","fileBlobId":"5776cd87617eacec3bc00ebcf530d1924026033eda852f706c1a675a98915826","title":{"en":"API Doc Live Demo"},"format":"IN_PERSON","location":"Lugano, Switzerland","timeZone":"Europe/Zurich","sameDay":false,"contentLanguages":[{"locale":"en","displayLanguage":"English"}]}]
http=200
要点解读:
title/formattedBeginDate等字段是多语言 Map 结构({"en": "...", "de": "..."}),集成方按自己的 locale 取值,缺省回退英文;datesWithOffset给出毫秒时间戳与时区偏移秒数,适合程序化排序;- 只有
status=PUBLIC的活动才会出现在此列表(草稿活动实测不可见,见 9.4 节对照)。
5.2 单活动详情:GET /api/v2/public/event/{name}
{name} 为活动 shortName(URL 标识):
curl -sk -w '\nhttp=%{http_code}\n' https://localhost:8443/api/v2/public/event/democonf2026 | head -c 400
实测输出(节选):
{"mapUrl":"","description":{"en":"<p>Demo conference created via REST API</p>\n"},"bankAccount":"","bankAccountOwner":[""],"formattedBeginDate":{"en":"Tue 01 Dec 2026"},"formattedBeginTime":{"en":"09:00"},"formattedEndDate":{"en":"Wed 02 Dec 2026"},"formattedEndTime":{"en":"18:00"},"invoicingConfiguration":{"userCanDownloadReceiptOrInvoice":true,"e
比列表多了 description(HTML 正文)、invoicingConfiguration(开票配置)等完整字段。活动不存在时返回 404 JSON 错误体(与第八章错误格式一致)。
5.3 票种列表:GET /api/v2/public/event/{name}/ticket-categories
curl -sk -w '\nhttp=%{http_code}\n' https://localhost:8443/api/v2/public/event/democonf2026/ticket-categories | head -c 400
实测输出(节选):
{"ticketCategories":[{"description":{"en":"<p>Standard ticket</p>\n"},"id":2,"name":"Standard","ticketAccessType":"INHERIT","bounded":true,"maximumSaleableTickets":5,"free":false,"formattedFinalPrice":"100.00","hasDiscount":false,"formattedDiscountedPrice":"","ordinal":1,"expired":false,"saleInFuture":false,"containsPendingTickets":false,"formattedInception":{"en":"20.10.2026 00:00"},"formattedExpiration":{"en":"25.11.2026 23:59"},"saleableAndLimitNotReached":true,"accessRestricted":false,"soldOutOrLimitReached":false,"availableTickets":null,"displayTaxInformation":true}],"expiredCategories":[],"additionalServices":[],"waitingList":false,"preSales":false,"ticketCategoriesForWaitingList":[]}
http=200
id 字段(本例为 2)是后续购票请求 categories 数组里要用的票种 ID;saleableAndLimitNotReached 可直接用作「是否可售」的展示开关。
5.4 活动日历订阅:GET /api/v2/public/event/{name}/calendar/{locale}
返回标准 iCalendar 格式,可直接被日历软件订阅:
curl -sk -w '\nhttp=%{http_code}\n' 'https://localhost:8443/api/v2/public/event/democonf2026/calendar/en' | head -12
实测输出(节选,实测前 12 行):
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Alf.io//Alf.io v2.0//EN
METHOD:PUBLISH
BEGIN:VEVENT
UID:a370ab6b-c0a0-47df-85c6-bc669de7ff45
DTSTAMP:20261023T192150Z
SUMMARY:DemoConf 2026
DESCRIPTION:De
http=200
集成时把该 URL 直接嵌入官网的「添加到日历」链接即可,无需解析处理。
5.5 写操作端点的 CSRF 行为(重要取证)
公开域还有一批 POST 端点(购票预留、优惠码校验、等待名单订阅等,完整清单见表 5-1)。本节用实测证据说明:在 2.0-M5-2609 构建上,纯脚本客户端调用这些 POST 会得到 404,其真实原因是 CSRF 拦截后的转发,而非端点不存在。
先给出应用内置的公开域端点清单(与应用控制器映射逐一核对):
| 端点 | 方法 | 用途 | 本指南实测 |
|---|---|---|---|
/api/v2/public/events | GET | 活动列表 | ✅ 5.1 |
/api/v2/public/event/{name} | GET | 活动详情 | ✅ 5.2 |
/api/v2/public/event/{name}/ticket-categories | GET | 票种列表 | ✅ 5.3 |
/api/v2/public/event/{name}/calendar/{locale} | GET | iCal 日历 | ✅ 5.4 |
/api/v2/public/event/{name}/validate-code | GET | 促销码校验 | 清单核对 |
/api/v2/public/event/{name}/code/{code} | GET | 促销码信息 | 清单核对 |
/api/v2/public/event/{name}/reserve-tickets | POST | 创建票品预留 | ⚠️ 5.5 取证 |
/api/v2/public/event/{name}/check-discount | POST | 预留优惠校验 | ⚠️ 5.5 取证 |
/api/v2/public/event/{name}/waiting-list/subscribe | POST | 等待名单订阅 | 清单核对 |
/api/v2/public/subscription/{id} | POST | 订阅型票预留 | 清单核对 |
/api/v2/public/info | GET | 实例信息 | ❌ 本版本 404(见 10.6) |
💡 技术拓展:CSRF(跨站请求伪造)防护的常见策略对比——
策略 原理 取舍 同步器令牌(Session Token) 令牌存服务端会话,表单/请求头回传比对 安全性高但强依赖会话,纯 API 客户端不友好(Alf.io 管理域即此方案) 双提交 Cookie(Double Submit) 令牌同时放 Cookie 与请求头,服务端比对两者 无需会话存储,适合前后端分离 SameSite Cookie 属性 浏览器层限制跨站携带 Cookie 零代码成本,但只对现代浏览器生效 令牌认证头(apikey/Bearer) 无 Cookie 即无 CSRF 攻击面 对 API 最彻底,Alf.io 对密钥认证链直接关闭 CSRF 本文实测:Alf.io 2.0-M5 的公开域写操作沿用会话式同步器令牌(GET 时通过
XSRF-TOKEN响应头发布令牌,写时以X-CSRF-TOKEN回传),密钥认证域则整体豁免。服务端集成因此应选择密钥通道。
取证第一步:观察公开域 GET 响应中令牌的发布方式。带会话保持的 GET 请求并Dump响应头:
rm -f /tmp/e2e.txt
curl -sk -c /tmp/e2e.txt -D /tmp/hdr.txt -o /dev/null https://localhost:8443/api/v2/public/event/democonf2026/ticket-categories
grep -iE '^(HTTP|Set-Cookie|XSRF)' /tmp/hdr.txt
实测输出:
HTTP/1.1 200 OK
XSRF-TOKEN: NkzsShYOvCvhIAdC87mc9rrnSDgMyKwpNIGDIMGgZBZmtok8U3vaf3Nojx3MEGV6xpSol9-BZVk_qcoEUOK3EffEXHVVgLpa
应用通过 XSRF-TOKEN 响应头发布令牌。注意此时没有任何 Set-Cookie 头——纯 API 的 GET 不会建立服务端会话,令牌因此无处持久保存。
取证第二步:把发布的令牌原样回传,调用购票预留端点:
TOK=$(grep -i '^XSRF-TOKEN:' /tmp/hdr.txt | sed 's/.*: *//' | tr -d '\r')
curl -sk -b /tmp/e2e.txt -w '\nhttp=%{http_code}\n' -X POST \
-H 'Content-Type: application/json' -H "X-CSRF-TOKEN: $TOK" \
-d '{"categories":[{"id":2,"quantity":1}]}' \
https://localhost:8443/api/v2/public/event/democonf2026/reserve-tickets
实测输出:
{"timestamp":"2026-10-23T19:24:51.366+00:00","status":404,"error":"Not Found","path":"/api/v2/public/event/democonf2026/reserve-tickets"}
http=404
仍是 404。取证第三步(归因):换一个不在购票人机验证过滤器后面的 POST 端点 check-discount,用完全相同的「会话 + 令牌」方式调用:
curl -sk -b /tmp/e2e.txt -w '\nhttp=%{http_code}\n' -X POST \
-H 'Content-Type: application/json' -H "X-CSRF-TOKEN: $TOK" \
-d '{"code":"NOTEXIST"}' \
https://localhost:8443/api/v2/public/event/democonf2026/check-discount
实测输出:
{"timestamp":"2026-10-23T19:25:37.386+00:00","status":404,"error":"Not Found","path":"/api/v2/public/event/democonf2026/check-discount"}
http=404
非购票端点同样 404,排除了「购票专属过滤器拦截」的可能,归因收敛到 CSRF/会话层。
机制解释(来自应用安全配置源码 AbstractFormBasedWebSecurity,与上述实测完全吻合):该构建的 CSRF 令牌是会话绑定式(HttpSessionCsrfTokenRepository,令牌存于服务端会话);公开域 GET 虽会发布令牌到 XSRF-TOKEN 响应头,但纯 API 客户端拿不到对应的服务端会话,POST 时令牌比对必然失败;CSRF 拒绝后,应用的访问拒绝处理器将请求转发到 /session-expired 页面,而 2.0-M5-2609 构建中没有该路径的映射,于是最终呈现为 404 JSON(path 字段保留原始请求路径)。换句话说:404 是「CSRF 拒绝 → 转发目标不存在」的叠加结果,reserve-tickets 端点本身存在且映射正确(同一控制器类的全部 GET 端点均 200 可证)。
集成结论:服务端集成不要依赖公开域 POST。购票类写流程有两种正确通道——① 浏览器/内嵌 WebView 走正常页面会话( Alf.io 前端 JS 自动处理 CSRF);② 服务端自动化用第六章的 v1 管理 API(会话式预留接口 /admin/api/reservation/event/*/* 系列)完成。
六、管理 API 域详解(/api/v1/admin 与 /admin/api)
管理域是服务端集成的主通道。本章按「活动域 → 系统域 → 用户域」给出实测端点,每个端点都带真实请求与回显;未逐项实测的端点以清单形式列出并标注来源。
6.1 端点总览
以下清单来自应用内置控制器映射(v1 EventApiV1Controller、系统 SystemApiController、后台 UsersApiController/EventApiController):
| 域 | 端点 | 方法 | 认证 | 实测情况 |
|---|---|---|---|---|
| 活动 | /api/v1/admin/event/create | POST | Bearer / apikey | ✅ 6.2 全流程 |
| 活动 | /api/v1/admin/event/list | GET | apikey(按组织过滤)/ Bearer | ✅ 4.2 |
| 活动 | /api/v1/admin/event/{slug}/stats | GET | apikey / Bearer | ✅ 4.2 |
| 活动 | /api/v1/admin/event/{slug}/download-attendees | GET | apikey / Bearer | 清单核对 |
| 活动 | /api/v1/admin/event/{slug} | DELETE | apikey / Bearer | 清单核对 |
| 活动 | /api/v1/admin/event/update/{slug} | POST | apikey / Bearer | 清单核对 |
| 活动 | /api/v1/admin/event/{slug}/subscriptions | GET/PUT | apikey / Bearer | 清单核对 |
| 活动 | /api/v1/admin/event/{slug}/check-in-log | GET | apikey / Bearer | 清单核对 |
| 系统 | /api/v1/admin/system/organization/list | GET | Bearer(系统密钥) | ✅ 4.1 |
| 系统 | /api/v1/admin/system/organization/create | POST | Bearer(系统密钥) | ✅ 4.1 |
| 后台 | /admin/api/users/new | POST | 管理会话 + CSRF | ✅ 7.3 |
| 后台 | /admin/api/events/{id}/status?active=true | PUT | 管理会话 + CSRF | ✅ 9.3 |
| 后台 | /admin/api/organizations、/admin/api/users 等 | GET/POST | 管理会话 / apikey | 清单核对 |
💡 说明:
/admin/api/**中标记「清单核对」的端点结构上同样支持 apikey 认证(同一令牌认证链),但本指南仅对表中打了 ✅ 的端点提供了实测回显;接入前请按第八章的错误码约定先行验证。
6.2 创建活动:POST /api/v1/admin/event/create(含完整排障实录)
这是集成中最有价值的端点,也是踩坑最密集的一个。请求体结构(字段来自应用模型 EventCreationRequest):
- 顶层:
title、slug(URL 标识,可省略自动生成)、description(数组,元素{lang, body})、format(IN_PERSON/ONLINE/HYBRID)、location({fullAddress, coordinate:{latitude, longitude}})、timezone、startDate/endDate(LocalDateTime格式)、websiteUrl、termsAndConditionsUrl、privacyPolicyUrl、imageUrl(必须,服务器可回源抓取的图片 URL,≤200KB)、tickets; tickets:freeOfCharge、currency、max(总票量)、taxPercentage、taxIncludedInPrice、paymentMethods(如["OFFLINE","ON_SITE"])、categories数组;categories元素:name、description(必须为数组,可为空数组,缺省将触发 500)、price、maxTickets、startSellingDate、endSellingDate等。
先把本次集成要提交的完整请求体写入文件(即最终成功版本,后续排障环节在此基础上做对照修改):
cat > /tmp/event.json <<'EOF'
{
"title": "Integration Guide Demo",
"slug": "intgdemo2026",
"description": [{"lang": "en", "body": "<p>Event created by the integration guide</p>"}],
"format": "IN_PERSON",
"location": {"fullAddress": "Lugano, Switzerland", "coordinate": {"latitude": "46.0037", "longitude": "8.9511"}},
"timezone": "Europe/Zurich",
"startDate": "2027-03-10T09:00:00",
"endDate": "2027-03-11T18:00:00",
"websiteUrl": "https://alfio.example.local",
"termsAndConditionsUrl": "https://alfio.example.local/terms",
"privacyPolicyUrl": "https://alfio.example.local/privacy",
"imageUrl": "http://localhost:8099/outline.png",
"tickets": {
"freeOfCharge": false,
"currency": "EUR",
"max": 100,
"taxPercentage": 0,
"taxIncludedInPrice": false,
"paymentMethods": ["OFFLINE", "ON_SITE"],
"categories": [{
"name": "Standard",
"description": [],
"price": 50,
"maxTickets": 100,
"startSellingDate": "2026-10-20T00:00:00",
"endSellingDate": "2027-03-01T23:59:00"
}]
}
}
EOF
排障实录一:imageUrl 缺失 → invalid.imageUrl。生成去掉 imageUrl 行的对照请求体并提交:
grep -v '"imageUrl"' /tmp/event.json > /tmp/event-noimg.json
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' -H 'Content-Type: application/json' \
-d @/tmp/event-noimg.json https://localhost:8443/api/v1/admin/event/create
实测输出:
"[{\"fieldName\":\"\",\"code\":\"invalid.imageUrl\",\"description\":\"Invalid Image URL\"}]"
http=400
imageUrl 是必填字段,缺失即被 400 校验拦截。
排障实录二:imageUrl 指向自签名 HTTPS 地址 → invalid.image。把图片地址换成与应用同域的自签名 HTTPS 地址再试:
sed 's|http://localhost:8099/outline.png|https://localhost:8443/images/sample-logo.png|' /tmp/event.json > /tmp/event-https.json
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' -H 'Content-Type: application/json' \
-d @/tmp/event-https.json https://localhost:8443/api/v1/admin/event/create
实测输出:
"[{\"fieldName\":\"\",\"code\":\"invalid.image\",\"description\":\"Image is either missing or too big (max 200kb)\"}]"
http=400
同时服务日志(journalctl -u alfio)出现证书错误:
Caused by: javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
结论:imageUrl 由服务端回源抓取,自签名 HTTPS 抓取失败即报 invalid.image。解决办法:把图片放在服务器能正常抓取的 HTTP(S) 地址(自有静态站点/对象存储)。本机演示起一个临时静态服务(图片从应用自带资源中取):
mkdir -p /tmp/img
unzip -o -j /opt/alfio/alfio-2.0-M5-2609-boot.jar 'BOOT-INF/classes/images/outline.png' -d /tmp/img
cd /tmp/img && nohup python3 -m http.server 8099 >/dev/null 2>&1 &
curl -s -o /dev/null -w 'img served: %{http_code} %{size_download}\n' http://localhost:8099/outline.png
实测输出:
img served: 200 2411
排障实录三:分类缺 description → 500 unexpected exception。图片可达后,生成删除分类 description 字段的对照请求体并提交:
python3 -c "import json; d=json.load(open('/tmp/event.json')); del d['tickets']['categories'][0]['description']; json.dump(d, open('/tmp/event-nodesc.json','w'))"
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' -H 'Content-Type: application/json' \
-d @/tmp/event-nodesc.json https://localhost:8443/api/v1/admin/event/create
实测输出:
"unexpected exception"
http=500
服务日志给出真实原因:
2026-10-23T19:29:02.903Z ERROR 14318 --- [Alf.io] [tp1824931880-37] a.c.a.ApiControllerExceptionHandler : unexpected exception
java.lang.NullPointerException: Cannot invoke "java.util.List.stream()" because "this.description" is null
分类的 description 字段必须提供(空数组即可)。
最终成功调用(使用开头的 /tmp/event.json,其中图片地址指向排障实录二中已就绪的本机静态服务):
curl -sk -w '\nhttp=%{http_code}\n' -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' -H 'Content-Type: application/json' \
-d @/tmp/event.json \
https://localhost:8443/api/v1/admin/event/create
实测输出:
"intgdemo2026"
http=200
成功时返回活动 slug 字符串。注意 startDate/endDate 用完整的 LocalDateTime 形式(2027-03-10T09:00:00);枚举值拼写错误(如把支付方式写成不存在的枚举)会得到 "bad request" 400。
七、管理会话与 CSRF:会话式调用
个别运维动作(创建 API 客户端账号、切换活动发布状态)走管理后台的会话式 API。本章复刻浏览器的完整会话流程并实测两个代表性调用。
7.1 会话登录三步流
第一步:GET 登录页,取得会话 Cookie 与表单 CSRF 令牌:
rm -f /tmp/cj6.txt
curl -sk -c /tmp/cj6.txt https://localhost:8443/authentication -o /tmp/auth6.html
CSRF=$(sed -n 's/.*name="_csrf" type="hidden" value="\([^"]*\)".*/\1/p' /tmp/auth6.html | head -1)
echo "登录页 CSRF 令牌长度: ${#CSRF}"
实测输出:
登录页 CSRF 令牌长度: 96
登录页 HTML 中有 <input name="_csrf" type="hidden" value="...">,令牌约 96 字符。
第二步:POST 表单登录(用户名 + 密码 + CSRF 令牌),会话 Cookie 由 -c/-b 文件自动续存:
curl -sk -b /tmp/cj6.txt -c /tmp/cj6.txt -o /dev/null -w '%{http_code}\n' -X POST https://localhost:8443/authenticate \
--data-urlencode 'username=admin' \
--data-urlencode 'password=NNtHfOzVERRyVRB6j' \
--data-urlencode "_csrf=$CSRF"
实测输出:
302
302 即登录成功后的重定向。
第三步:进入后台首页,提取后续 API 调用要用的会话级 CSRF 令牌(后台页面 meta 标签中,或任意 /admin/api GET 响应的 XSRF-TOKEN 头):
ACSRF=$(curl -sk -b /tmp/cj6.txt https://localhost:8443/admin/ | sed -n 's/.*name="_csrf" content="\([^"]*\)".*/\1/p' | head -1)
echo "后台会话 CSRF 令牌长度: ${#ACSRF}"
实测输出:
后台会话 CSRF 令牌长度: 96
7.2 会话式调用范例:切换活动发布状态
创建出来的活动默认是草稿,公开域不可见。用会话 + CSRF 令牌调用状态切换端点(活动 id 用 SQL 查得,本例 intgdemo2026 为 4):
EID=$(PGPASSWORD=q1w2e3r4 psql -h localhost -U alfio -d alfio -t -A -c "select id from event where short_name='intgdemo2026'")
echo "EID=$EID"
curl -sk -b /tmp/cj6.txt -w '\nhttp=%{http_code}\n' -X PUT \
-H "X-CSRF-TOKEN: $ACSRF" \
"https://localhost:8443/admin/api/events/$EID/status?active=true"
实测输出:
EID=4
"OK"
http=200
"OK" + 200 表示发布成功。若不带 X-CSRF-TOKEN 或令牌与会话不匹配,得到的是第五章 5.5 同款的 404 转发现象(管理域的会话有效,转发目标同样缺失);带 X-Requested-With: XMLHttpRequest 头时则会得到直接的 403。
7.3 创建 API 客户端账号:POST /admin/api/users/new
用同样的会话流程创建 API_KEY 类型客户端(此前 4.2 节使用的客户端即这样创建)。请求体字段:organization(归属组织 id)、username(留空自动生成,即密钥本体)、role(必须为 API_CONSUMER)、description:
curl -sk -w '\nhttp=%{http_code}\n' -b /tmp/cj6.txt -H "X-CSRF-TOKEN: $ACSRF" -H 'Content-Type: application/json' \
-X POST 'https://localhost:8443/admin/api/users/new?baseUrl=https%3A%2F%2Falfio.example.local' \
-d '{"organization":2,"username":"","role":"API_CONSUMER","description":"API doc integration"}'
实测输出:
{"password":"","uniqueId":"19949807-f299-421e-99b9-300cf345cc5d","qrCode":null,"id":7,"type":"API_KEY","enabled":true,"username":"8a54f655-86b1-4855-848b-ba57f2e8a74b","firstName":"apikey","lastName":"","emailAddress":"","validToEpochSecond":null,"validTo":null,"description":"API doc integration"}
http=200
username 字段的值(UUID 形态)就是 API 密钥,仅在创建响应中出现一次,务必立即保存。
⚠️ 注意:
role若写成API_KEY会得到 500 错误"No enum constant alfio.model.user.Role.API_KEY"——正确的角色枚举值是API_CONSUMER,创建出来的用户类型(type字段)才显示为API_KEY。
八、通用约定与错误码
本章汇总跨端点一致的约定:错误响应结构、状态码语义、数据格式约定,便于集成代码统一处理。
8.1 错误响应结构
所有 JSON 错误响应都是统一的四键结构(Spring Boot 默认错误属性):
{"timestamp":"2026-10-23T19:25:37.386+00:00","status":404,"error":"Not Found","path":"/api/v2/public/event/democonf2026/check-discount"}
timestamp:ISO-8601 带时区时间戳;status:HTTP 状态码;error:状态短语;path:原始请求路径(即使错误发生在转发后,path 仍是调用方请求的地址,这也是第五章鉴别 CSRF 转发 404 的关键线索)。- 参数校验类错误(400)的响应体是校验错误数组,如
[{"fieldName":"","code":"invalid.image","description":"Image is either missing or too big (max 200kb)"}],按code字段编程处理。 - 个别校验失败返回简单字符串(如
"bad request"),集成时按 JSON 字符串类型兜底解析。
8.2 状态码实测对照表
| 状态码 | 实测触发场景 | 处理建议 |
|---|---|---|
| 200 | 全部成功调用 | 正常解析 |
| 302 | 根路径重定向、登录成功重定向 | 跟随即可 |
| 400 | 参数校验失败(invalid.image / bad request 等) | 解析错误数组,修正请求 |
| 403 | 未认证访问受保护端点(无令牌/令牌权限不足) | 检查 Authorization 头与密钥权限域 |
| 404 | 资源不存在;或 CSRF 拒绝后的转发现象(POST 类) | 先核实资源存在性,再查 CSRF/会话(5.5 鉴别法) |
| 500 | 服务端异常(如分类缺 description 的 NPE、"No enum constant") | 查服务日志定位请求体问题 |
8.3 数据格式约定
- 多语言字段:统一为
{locale: 文本}Map(如"title": {"en": "DemoConf 2026"}),写入时用[{lang, body}]数组(活动/分类 description),读取时为 Map; - 金额:响应中以元为单位的小数(
"price": 50.00),请求分类价格用数字("price": 50); - 时间:请求用
LocalDateTime(2027-03-10T09:00:00,按活动时区解释),响应中同时出现datesWithOffset(毫秒时间戳 + 时区偏移)与formatted*(各语言格式化串); - 分页:管理域列表端点(如 event/list)实测返回全量数组,无统一的 page/size 参数;组织隔离即天然的「过滤」维度(apikey 客户端只见本组织数据)。
九、端到端集成实战
本章把前述能力串成一条完整的集成流水线:准备图片 → 创建活动 → 发布 → 公开端验证。除图片准备外全部为已实测命令。
9.1 准备服务器可抓取的活动图片
Alf.io 创建活动时要求一个服务端可回源抓取的图片 URL(≤200KB)。生产环境用自有静态站点/对象存储;本机演示用临时静态服务(复用 6.2 节已就绪的 /tmp/img,若未启动按 6.2 节命令重启):
curl -s -o /dev/null -w 'img served: %{http_code} %{size_download}\n' http://localhost:8099/outline.png
实测输出:
img served: 200 2411
9.2 创建活动(v1 管理 API)
见 6.2 节「最终成功调用」,返回:
"intgdemo2026"
http=200
9.3 发布活动(管理会话切换状态)
见 7.2 节实测,返回:
EID=4
"OK"
http=200
9.4 公开域验证:发布前后对照
发布前调用公开详情端点得到 404(草稿不可见);发布后重新调用:
curl -sk -w '\nhttp=%{http_code}\n' https://localhost:8443/api/v2/public/event/intgdemo2026 | head -c 220
实测输出:
{"mapUrl":"","description":{"en":"<p><p>Event created by the integration guide</p></p>\n"},"bankAccount":"","bankAccountOwner":[""],"formattedBeginDate":{"en":"Wed 10 Mar 2027"},"formattedBeginTime":{"en":"09
http=200
200 且返回完整详情,活动已对公众可见。再用管理密钥从组织域复核状态:
curl -sk -H 'Authorization: apikey bc333b51-b8c1-4fc9-8d42-d5ade41af9de' \
https://localhost:8443/api/v1/admin/event/list | python3 -c 'import json,sys; print([(e["shortName"], e["status"]) for e in json.load(sys.stdin)])'
实测输出:
[('d5apidemo', 'PUBLIC'), ('intgdemo2026', 'PUBLIC')]
两个活动均为 PUBLIC,端到端集成闭环完成。
💡 说明:观察 9.4 回显中 description 出现了
<p><p>...的双重转义——通过 API 写入的 HTML 正文会被当作纯文本再做一次转义展示。若正文需要富文本样式,建议创建后用管理后台编辑器修正,或直接传纯文本。
十、常见问题(FAQ)
Q1:POST /api/v2/public/... 一直返回 404,端点是不是不存在?
不是。第五章 5.5 的取证表明这是 CSRF 拒绝后转发到本版本不存在的 /session-expired 页面所致,端点映射本身存在。鉴别方法:① 同控制器的 GET 端点应为 200;② 错误体 path 保留原始路径;③ 改用管理会话方式(第七章)或 v1 管理 API 即可绕开。另:若客户端带了 X-Requested-With: XMLHttpRequest 头,CSRF 拒绝会表现为直接的 403 而非 404,这也可作为鉴别的旁证。
Q2:创建活动报 invalid.image / invalid.imageUrl?
imageUrl 由 Alf.io 服务端回源抓取。invalid.imageUrl = 缺失或格式非法;invalid.image = 抓取失败(自签名 HTTPS 会出现 PKIX 证书错误,见 6.2 排障实录一)或图片超过 200KB。把图片放到带有效证书的地址即可。
Q3:创建活动报 "unexpected exception" 500?
查服务日志(journalctl -u alfio)。实测最常见原因是票种分类缺 description 字段(NPE),补上 "description": [] 即可,见 6.2 排障实录二。
Q4:创建 API 客户端报 "No enum constant alfio.model.user.Role.API_KEY"?
role 字段写错了。正确值是 API_CONSUMER,见 7.3 节。
Q5:apikey 客户端查不到某个活动?
API 客户端严格按所属组织隔离数据(4.2/4.3 节实测),且 event/list 只含本组织活动。跨组织管理需用系统密钥(Bearer)的系统域端点。
Q6:官方文档提到的 GET /api/v2/public/info 在本版本可用吗? 实测不可用:
curl -sk -w '\nhttp=%{http_code}\n' https://localhost:8443/api/v2/public/info
实测输出:
{"timestamp":"2026-10-23T13:53:25.342+00:00","status":404,"error":"Not Found","path":"/api/v2/public/info"}
http=404
该端点在 2.0-M5-2609 里程碑构建中未提供,集成时不要依赖。
Q7:有没有 Swagger UI / OpenAPI 文档可以看? 本构建未内置(第三章实测三个常见端点均 404)。端点结构以官方文档与应用内置控制器为准,本指南表 5-1 与 6.1 已整理。
十一、参考链接
- Alf.io 官方文档站:https://docs.alf.io/
- Alf.io GitHub 仓库(alfio-event/alf.io):https://github.com/alfio-event/alf.io
- 本专栏系列文档:《Alf.io 2.0-M5-2609 介绍与选型指南》、《Ubuntu 22.04 安装 Alf.io 2.0-M5-2609 指南》、《Alf.io 2.0-M5-2609 配置参数详解》