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.ioAlf.io票务2026/10/24

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/eventsGET活动列表✅ 5.1
/api/v2/public/event/{name}GET活动详情✅ 5.2
/api/v2/public/event/{name}/ticket-categoriesGET票种列表✅ 5.3
/api/v2/public/event/{name}/calendar/{locale}GETiCal 日历✅ 5.4
/api/v2/public/event/{name}/validate-codeGET促销码校验清单核对
/api/v2/public/event/{name}/code/{code}GET促销码信息清单核对
/api/v2/public/event/{name}/reserve-ticketsPOST创建票品预留⚠️ 5.5 取证
/api/v2/public/event/{name}/check-discountPOST预留优惠校验⚠️ 5.5 取证
/api/v2/public/event/{name}/waiting-list/subscribePOST等待名单订阅清单核对
/api/v2/public/subscription/{id}POST订阅型票预留清单核对
/api/v2/public/infoGET实例信息❌ 本版本 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/createPOSTBearer / apikey✅ 6.2 全流程
活动/api/v1/admin/event/listGETapikey(按组织过滤)/ Bearer✅ 4.2
活动/api/v1/admin/event/{slug}/statsGETapikey / Bearer✅ 4.2
活动/api/v1/admin/event/{slug}/download-attendeesGETapikey / Bearer清单核对
活动/api/v1/admin/event/{slug}DELETEapikey / Bearer清单核对
活动/api/v1/admin/event/update/{slug}POSTapikey / Bearer清单核对
活动/api/v1/admin/event/{slug}/subscriptionsGET/PUTapikey / Bearer清单核对
活动/api/v1/admin/event/{slug}/check-in-logGETapikey / Bearer清单核对
系统/api/v1/admin/system/organization/listGETBearer(系统密钥)✅ 4.1
系统/api/v1/admin/system/organization/createPOSTBearer(系统密钥)✅ 4.1
后台/admin/api/users/newPOST管理会话 + CSRF✅ 7.3
后台/admin/api/events/{id}/status?active=truePUT管理会话 + 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>&lt;p&gt;Event created by the integration guide&lt;/p&gt;</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>&lt;p&gt;... 的双重转义——通过 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 已整理。

十一、参考链接