Fizzy Cards API 实战指南:看板卡片的全生命周期管理
【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy
卡片(Card)是 Fizzy 看板中任务与工作项的基本单元——它们可以被组织进工作流列(Column)、打上标签、指派给用户、附加评论,还可以经历从草稿、分诊(Triage)、关闭到 Golden 标记的完整状态流转。本文以 Cards API 参考 为骨架,逐端点讲解如何通过 HTTP 接口完成卡片的查询、创建、更新、删除与状态操作,并结合本仓库的 路由定义、CardsController、JSON 序列化模板 与 Filter 模型 源码,说明每个参数背后的真实实现。读完本文,你将能够独立编写脚本或机器人,对 Fizzy 卡片做完整的读写与控制。
一、前置知识:认证、Base URL 与通用机制
所有 Cards 端点都隶属于 Fizzy API,统一使用/:account_slug作为路径前缀(例如示例中的897362094),并以 JSON 格式交换数据。调用前请先阅读 API 总览 与 认证指南,掌握以下通用约定:
- 认证方式:使用
Authorization: Bearer <token>头携带个人访问令牌(Personal Access Token),适用于脚本与集成;或使用 Magic Link 会话令牌(Cookie),适用于原生应用。令牌分为Read与Read + Write两种权限,写操作(创建、更新、删除等)要求具有写权限。 - ETag 缓存:大多数端点返回
ETag与Cache-Control头,后续请求带上If-None-Match可获得304 Not Modified,避免重复下载未变化的数据。 - 分页:所有列表端点均分页,页面大小是动态的(靠前页返回更少结果);若有更多数据,响应头会给出
Link: <...?page=2>; rel="next"。 - 列表参数:凡是接受多值列表的参数,都以
[]结尾,可重复传参,例如?tag_ids[]=tag1&tag_ids[]=tag2。 - 文件上传:
image等文件字段需改用multipart/form-data请求,且可与普通参数混用。 - 富文本:
description接受经过清洗(sanitize)的 HTML 输入,具体直传流程见 Rich Text 指南。
在 api_test.rb 中可以看到完整认证链路的集成测试:Bearer令牌通过HTTP_AUTHORIZATION请求头注入(bearer_token_env),无效令牌返回401 Unauthorized,而只读令牌执行写操作同样返回401。
二、列出卡片:GET /:account_slug/cards
该端点返回当前身份有权限访问的卡片分页列表,并通过查询参数进行多维筛选。这是 Cards API 最强大的端点,对应 Filter#cards 中的查询构建逻辑。
2.1 查询参数表
| 参数 | 说明 |
|---|---|
board_ids[] | 按看板 ID 筛选 |
tag_ids[] | 按标签 ID 筛选 |
assignee_ids[] | 按指派用户 ID 筛选 |
creator_ids[] | 按卡片创建者 ID 筛选 |
closer_ids[] | 按关闭卡片的用户 ID 筛选 |
card_ids[] | 精确到指定卡片 ID 列表 |
column_ids[] | 按工作流列 ID 筛选 |
indexed_by | 筛选索引:all(默认)、maybe、closed、not_now、stalled、postponing_soon、golden |
sorted_by | 排序方式:latest(默认)、newest、oldest |
assignment_status | 按指派状态筛选:unassigned |
creation | 按创建时间筛选:today、yesterday、thisweek、lastweek、thismonth、lastmonth、thisyear、lastyear |
closure | 按关闭时间筛选:取值同上 |
terms[] | 搜索关键词,用于全文检索过滤卡片 |
组合语义:重复的column_ids[]值之间是OR关系(例如?column_ids[]=a&column_ids[]=b表示列 a 或列 b 中的卡片);其余筛选条件之间以AND组合。
示例:
column_ids[]=03f...— 返回指定工作流列中的卡片。
2.2 参数背后的源码实现
从源码看,这些参数与 Filter::Params::PERMITTED_PARAMS 中声明的内容一一对应:
PERMITTED_PARAMS = [ :assignment_status, :indexed_by, :sorted_by, :creation, :closure, card_ids: [], column_ids: [], assignee_ids: [], creator_ids: [], closer_ids: [], board_ids: [], tag_ids: [], terms: [] ]筛选链在 Filter#cards 中按固定顺序叠加:先限定creator.accessible_cards.preloaded.published,再依次应用indexed_by、sorted_by、显式卡片 ID、Not Now/关闭状态、未指派、指派者、创建者、看板、标签、时间窗口、关闭者、搜索词与列 ID,最后distinct去重。这意味着接口的筛选能力与实际看板 UI 的筛选能力同源——同一个 Filter 模型同时服务于页面与 API。
indexed_by与sorted_by的取值映射见 Card#indexed_by / Card#sorted_by:
scope :indexed_by, ->(index) do case index when "stalled" then stalled when "postponing_soon" then postponing_soon when "closed" then closed when "maybe" then awaiting_triage when "not_now" then postponed.latest when "golden" then golden when "draft" then drafted else all end end scope :sorted_by, ->(sort) do case sort when "newest" then reverse_chronologically # created_at DESC when "oldest" then chronologically # created_at ASC when "latest" then latest # last_active_at DESC else latest end end可见:newest/oldest依据创建时间,latest依据最近活跃时间last_active_at;maybe实际是“待分诊”(awaiting triage),not_now则是“已推迟且按活跃度排序”。这些枚举定义在 Filter::Fields 中:INDEXES = %w[ all closed not_now stalled postponing_soon golden ],SORTED_BY = %w[ newest oldest latest ],默认值{ indexed_by: "all", sorted_by: "latest" }。creation/closure时间窗由 TimeWindowParser 解析为起止时间范围。
2.3 响应结构
[ { "id": "03f5vaeq985jlvwv3arl4srq2", "number": 1, "title": "First!", "status": "published", "description": "Hello, World!", "description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>", "image_url": null, "has_attachments": false, "tags": ["programming"], "golden": false, "last_active_at": "2025-12-05T19:38:48.553Z", "created_at": "2025-12-05T19:38:48.540Z", "url": "http://app.fizzy.localhost:3006/897362094/cards/4", "board": { "id": "03f5v9zkft4hj9qq0lsn9ohcm", "name": "Fizzy", "all_access": true, "created_at": "2025-12-05T19:36:35.534Z", "auto_postpone_period_in_days": 30, "url": "http://app.fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm", "creator": { "id": "03f5v9zjw7pz8717a4no1h8a7", "name": "David Heinemeier Hansson", "role": "owner", "active": true, "email_address": "david@example.com", "created_at": "2025-12-05T19:36:35.401Z", "url": "http://app.fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7" } }, "creator": { "id": "03f5v9zjw7pz8717a4no1h8a7", "name": "David Heinemeier Hansson", "role": "owner", "active": true, "email_address": "david@example.com", "created_at": "2025-12-05T19:36:35.401Z", "url": "http://app.fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7" }, "comments_url": "http://app.fizzy.localhost:3006/897362094/cards/4/comments", "reactions_url": "http://app.fizzy.localhost:3006/897362094/cards/4/reactions" } ]列表项序列化模板见 app/views/cards/_card.json.jbuilder:description输出纯文本(to_plain_text),description_html输出富文本 HTML;image_url在有头图时给出 URL;tags为标签标题排序后的数组;assignees会附带最多 5 个指派用户并给出has_more_assignees标记(该字段在文档示例中未展示,但列表响应中实际存在,可据此判断是否需要额外拉取)。url、comments_url、reactions_url提供后续操作的导航入口。
三、获取单张卡片:GET /:account_slug/cards/:card_number
通过卡片编号(number)获取单张卡片的完整信息。注意 URL 中不是 UUID,而是递增的短编号——从源码看 Card#to_param 返回number.to_s,且 CardsController#set_card 使用Current.user.accessible_cards.find_by!(number: params[:id])按编号查询,同时受访问控制约束。
{ "id": "03f5vaeq985jlvwv3arl4srq2", "number": 1, "title": "First!", "status": "published", "description": "Hello, World!", "description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>", "image_url": null, "has_attachments": false, "tags": ["programming"], "closed": false, "golden": false, "last_active_at": "2025-12-05T19:38:48.553Z", "created_at": "2025-12-05T19:38:48.540Z", "url": "http://app.fizzy.localhost:3006/897362094/cards/4", "board": { "id": "03f5v9zkft4hj9qq0lsn9ohcm", "name": "Fizzy", "all_access": true, "created_at": "2025-12-05T19:36:35.534Z", "auto_postpone_period_in_days": 30, "url": "http://app.fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm", "creator": { "id": "03f5v9zjw7pz8717a4no1h8a7", "name": "David Heinemeier Hansson", "role": "owner", "active": true, "email_address": "david@example.com", "created_at": "2025-12-05T19:36:35.401Z", "url": "http://app.fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7" } }, "column": { "id": "03f5v9zkft4hj9qq0lsn9ohcn", "name": "In Progress", "color": { "name": "Lime", "value": "var(--color-card-4)" }, "created_at": "2025-12-05T19:36:35.534Z" }, "creator": { "id": "03f5v9zjw7pz8717a4no1h8a7", "name": "David Heinemeier Hansson", "role": "owner", "active": true, "email_address": "david@example.com", "created_at": "2025-12-05T19:36:35.401Z", "url": "http://app.fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7" }, "comments_url": "http://app.fizzy.localhost:3006/897362094/cards/4/comments", "reactions_url": "http://app.fizzy.localhost:3006/897362094/cards/4/reactions", "steps": [ { "id": "03f8huu0sog76g3s975963b5e", "content": "This is the first step", "completed": false }, { "id": "03f8huu0sog76g3s975969734", "content": "This is the second step", "completed": false } ] }注意:
closed字段表示卡片是否处于“Done”状态。column字段仅在卡片已分诊进某工作流列时出现;处于 "Maybe?"、"Not Now" 或 "Done" 状态的卡片不会有该字段。这一点在序列化模板中也有印证:app/views/cards/_card.json.jbuilder 中json.column ... if card.column为条件输出。单卡片响应还额外包含steps步骤清单(标题、是否完成),card.steps通过Multistep模块关联。
四、创建卡片:POST /:account_slug/boards/:board_id/cards
在指定看板中创建新卡片。与其它端点不同,创建动作挂在看板资源下(见 routes.rb 中resources :boards do resources :cards, only: :create end)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
title | string | 是 | 卡片标题 |
description | string | 否 | 富文本描述 |
status | string | 否 | 初始状态:published(默认)、drafted |
image | file | 否 | 卡片头图 |
tag_ids | array | 否 | 应用到卡片的标签 ID 数组 |
created_at | datetime | 否 | 覆盖创建时间戳(ISO 8601 格式) |
last_active_at | datetime | 否 | 覆盖最近活跃时间戳(ISO 8601 格式) |
请求示例:
{ "card": { "title": "Add dark mode support", "description": "We need to add dark mode to the app" } }响应:返回201 Created,响应头Location指向新创建的卡片。
源码印证:请求体必须包在card键下,这是因为 CardsController 开头声明了wrap_parameters :card, include: %i[ title description image created_at last_active_at ],允许顶层扁平参数自动包装;最终通过card_params(params.expect(card: [ :title, :description, :image, :created_at, :last_active_at ]))做强校验(Strong Parameters)。created_at与last_active_at的覆盖能力支持数据迁移与历史数据导入场景。注意从源码结构看,JSON 分支创建时状态被固定为"published"(HTML 分支则进入草稿流程),因此若需要drafted状态的卡片,可先创建后再用更新端点调整status。
五、更新卡片:PUT /:account_slug/cards/:card_number
更新已有卡片,请求体同样包在card键下。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
title | string | 否 | 卡片标题 |
description | string | 否 | 富文本描述 |
status | string | 否 | 卡片状态:drafted、published |
image | file | 否 | 卡片头图 |
tag_ids | array | 否 | 应用到卡片的标签 ID 数组 |
last_active_at | datetime | 否 | 覆盖最近活跃时间戳(ISO 8601 格式) |
请求示例:
{ "card": { "title": "Add dark mode support (Updated)" } }响应:返回更新后的完整卡片(结构同单卡片响应)。控制器侧 update 执行@card.update! card_params,JSON 格式下渲染:show模板;草稿发布/回退为草稿的status流转另有 publishes_controller.rb 与路由resource :publish支持。
六、删除与头图操作
6.1 删除卡片:DELETE /:account_slug/cards/:card_number
删除一张卡片。权限约束:只有卡片创建者或看板管理员可以删除。
响应:成功返回204 No Content。
源码中权限检查在 CardsController#ensure_permission_to_administer_card:
def ensure_permission_to_administer_card head :forbidden unless Current.user.can_administer_card?(@card) end即无权限时返回403 Forbidden,与 API 总览 的错误码约定一致。
6.2 移除头图:DELETE /:account_slug/cards/:card_number/image
移除卡片的头图,对应 images_controller.rb(路由resource :image)。成功返回204 No Content。
七、状态流转:关闭、重新打开与 Not Now
Fizzy 卡片存在多种非列状态,通过独立子资源端点切换:
| 操作 | 端点 | 效果 |
|---|---|---|
| 关闭卡片 | POST /:account_slug/cards/:card_number/closure | 进入 Done 状态,closed为true |
| 重新打开 | DELETE /:account_slug/cards/:card_number/closure | 移出 Done 状态 |
| 移入 Not Now | POST /:account_slug/cards/:card_number/not_now | 推迟到 “Not Now” 列表 |
三者成功均返回204 No Content。对应控制器为 closures_controller.rb 与 not_nows_controller.rb,路由在 routes.rb 中声明为resource :closure、resource :not_now。卡片关闭能力由Closeable、推迟能力由Postponable模块提供,这也解释了列表筛选里indexed_by=closed与indexed_by=not_now的含义。
八、移动与组织:换板、分诊、标签与指派
8.1 移动到其他看板:PUT /:account_slug/cards/:card_number/board
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
board_id | string | 是 | 目标看板 ID |
请求示例:
{ "board_id": "03f5v9zkft4hj9qq0lsn9ohcm" }响应:返回200 OK与移动后的卡片(结构与单卡片响应一致),board字段反映新看板。对应 boards_controller.rb;底层逻辑见 Card#move_to,它在一个事务中同时迁移卡片、卡片事件与评论事件到新看板,保证活动历史的连贯性。
8.2 分诊(Triage):POST / DELETE /:account_slug/cards/:card_number/triage
POST将卡片移入指定工作流列,参数column_id(string,必填)为目标列 ID;成功返回204 No Content。DELETE将卡片送回“待分诊”(Maybe?)状态;成功返回204 No Content。
分诊对应 triages_controller.rb,卡片列归属由Triageable模块管理。
8.3 切换标签:POST /:account_slug/cards/:card_number/taggings
在卡片上切换(toggle)一个标签:若标签不存在则自动创建。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
tag_title | string | 是 | 标签标题(开头的#会被剥除) |
成功返回204 No Content。对应 taggings_controller.rb,实际响应由 app/views/cards/taggings/create.turbo_stream.erb 等模板驱动(HTTP 层面返回空响应)。
8.4 切换指派:POST /:account_slug/cards/:card_number/assignments
切换某用户与卡片的指派关系(指派/取消指派)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
assignee_id | string | 是 | 要指派/取消指派的用户 ID |
成功返回204 No Content。对应 assignments_controller.rb;未指派筛选(assignment_status=unassigned)即判断卡片当前没有任何指派者。
九、关注与特殊标记:Watch 与 Golden
9.1 关注/取消关注:POST / DELETE /:account_slug/cards/:card_number/watch
POST:当前用户订阅该卡片的通知,成功返回204 No Content。DELETE:当前用户取消订阅该卡片的通知,成功返回204 No Content。
对应 watches_controller.rb 与Watchable模块,UI 侧的按钮状态由 app/views/cards/watches/_watch_button.html.erb 渲染。关注后,卡片上的后续活动(评论、指派等)会触发通知。
9.2 Golden 标记:POST / DELETE /:account_slug/cards/:card_number/goldness
POST:将卡片标记为 Golden(金票),成功返回204 No Content。DELETE:移除 Golden 标记,成功返回204 No Content。
对应 goldnesses_controller.rb 与Golden模块,响应中的golden布尔字段即由此而来;列表筛选的indexed_by=golden也只返回被标记的卡片。
十、实战建议与调用要点
- 善用 ETag 做增量同步:定时轮询
GET /:account_slug/cards/:card_number时,把上次响应的ETag放入If-None-Match;未变化时得到304,避免无谓的带宽与解析开销(参考 API 总览 的缓存章节)。 - 列表端点优先于逐个抓取:一次
GET /:account_slug/cards即可按看板、标签、指派者、时间窗等多维条件批量取卡,再通过Link: rel="next"分页翻完;动态页大小意味着前几页结果较少,属正常现象。 - 写操作注意令牌权限:创建、更新、删除及所有状态切换端点都需要
Read + Write令牌;权限不足会得到401(见 api_test.rb 中“changing data requires a write-endowed access token”测试)。 - 组合筛选的语义边界:只有
column_ids[]的多个值是 OR 关系,其余全部 AND;若需要“列 A 或列 B”,直接重复传参即可,无需构造复杂查询。 - 上传文件走 multipart:
image字段请用-F "card[image]=@/path/to/cover.jpg"形式提交(参考 API 总览 的文件上传章节),description富文本如需附带内嵌附件,请先阅读 Rich Text 指南 的直传流程。
通过以上 17 个端点,你可以完整覆盖 Fizzy 卡片的“创建 → 分诊 → 流转 → 指派/打标 → 关闭 → 删除”全生命周期,并将其嵌入自动化脚本、外部工具或机器人流程中。
【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考