solidtime v0.21.0 API 使用详解

solidtime v0.21.0 REST API 逐接口实测:Token 签发、组织/项目/任务/计时条目 CRUD 与启停计时,含 Passport 客户端初始化、color 小写校验、PATCH 批量语义等六个真实踩坑。

solidtime时间追踪自托管项目管理2026/10/24

solidtime v0.21.0 API 使用详解

目标:在真实部署的 solidtime v0.21.0 实例上,从认证、组织、项目、任务到计时条目,逐接口实测 REST API 的调用方法、请求响应形态与踩坑结论。实例由 Docker Compose 部署(见《solidtime v0.21.0 Docker Compose 部署指南》),全部请求均为 2026-10-24 实测记录。接口全集以官方 OpenAPI 3.1 规范为底本(54 个路径),本篇覆盖核心 CRUD 链路。

一、API 总览

  • Base URL:http://<APP_URL>/api/v1(本篇实例为 http://localhost:8137/api/v1,路径中的 /v1 由规范固定)
  • 协议:REST + JSON,规范版本 OpenAPI 3.1.0
  • 认证:Authorization: Bearer <token>(个人访问令牌);未认证返回 401:
curl -sS -w ' HTTP=%{http_code}\n' -H 'Accept: application/json' http://localhost:8137/api/v1/users/me
# 实测输出:
{"message":"Unauthenticated."} HTTP=401
  • 通用请求头:Accept: application/json(缺省时部分接口返回 HTML 错误页);写操作加 Content-Type: application/json
  • 列表类响应统一为 {data, links, meta} 信封(links/meta 承载分页),单对象响应为 {data: {...}}
  • 限流:认证用户 200 次/分钟、访客 60 次/分钟(APP_ENV=production 时强制,实测精准触发,见配置篇第 10 节)

1.1 v0.21.0 接口地图(实测涉及✓)

域路径方法
用户/v1/users/me ✓、/v1/users/me/memberships ✓、/v1/users/me/api-tokens、/v1/users/me/time-entries/active ✓GET/POST/DELETE
组织/v1/organizations ✓、/v1/organizations/{org} ✓POST/GET/PUT/DELETE
成员/v1/organizations/{org}/members ✓、invitations ✓GET/POST/DELETE
项目/v1/organizations/{org}/projects ✓GET/POST/PUT/DELETE
任务/v1/organizations/{org}/tasks ✓GET/POST/PUT/DELETE
计时/v1/organizations/{org}/time-entries ✓、time-entries/{id} ✓、aggregate、exportGET/POST/PATCH/PUT/DELETE
报表/v1/organizations/{org}/reports、charts/*GET/POST

二、认证:创建个人 API Token

2.1 前置:Passport 个人访问客户端(一次性,官方文档未提的坑)

全新实例第一次创建 Token 会报错,实测过程:

docker compose exec -T scheduler php artisan tinker --execute='...createToken(...)'
# 实测失败输出:
RuntimeException  Personal access client not found for 'users' user provider. Please create one.

原因:PASSPORT 密钥对(部署篇 2.4 节生成)只是签名材料,Passport 的个人访问客户端记录还需一次性创建:

docker compose exec -T scheduler php artisan passport:client --personal --name="solidtime-personal" --provider=users --no-interaction
# 实测输出:
   INFO  New client created successfully.

2.2 签发 Token(实测可用)

官方路径是网页端「用户设置 → API 令牌」;CLI 实测路径(真实执行):

docker compose exec -T scheduler php artisan tinker --execute='$u=\App\Models\User::where("email","admin@solidtime.local")->first(); $t=$u->createToken("d5-cli-token"); echo $t->accessToken;'

实测返回 1081 字符的 JWT 形态 Token(形如 eyJ0eXAiOiJKV1QiLCJhbGci...,仅创建时完整展示一次,请立即保存)。Token 管理接口:GET/POST /v1/users/me/api-tokens、POST /v1/users/me/api-tokens/{id}/revoke、DELETE /v1/users/me/api-tokens/{id}。

三、用户与组织

3.1 当前用户:GET /v1/users/me

curl -sS -H "Authorization: Bearer $TOK" -H 'Accept: application/json' http://localhost:8137/api/v1/users/me

实测响应(节选):

{"data":{"id":"eee4c078-6503-46cc-819c-82b29d165b08","name":"admin","email":"admin@solidtime.local","timezone":"UTC","week_start":"monday","currency":"EUR"}}

3.2 创建组织:POST /v1/organizations

curl -sS -X POST -H "Authorization: Bearer $TOK" -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -d '{"name":"demo"}' http://localhost:8137/api/v1/organizations

实测响应(节选):

{"data":{"id":"8b787d7d-65cd-4450-9197-b6fe00cd5f8d","name":"demo","is_personal":false,"currency":"EUR","currency_symbol":"€","number_format":"point-comma"}}

组织 ID(下文记 $ORG)从响应 data.id 获取。实测发现:CLI 建号后系统会自动创建一个名为「admin's Organization」的组织(is_personal=true),与手工创建的并存,多组织通过 PUT /v1/users/me/current-organization 切换。

3.3 成员身份:GET /v1/users/me/memberships

curl -sS -H "Authorization: Bearer $TOK" -H 'Accept: application/json' http://localhost:8137/api/v1/users/me/memberships

实测响应(节选):

{"data":[
  {"id":"9355d38b-b982-4f5e-8392-0e1e0b39d590","organization":{"id":"9d9f7e8e-...","name":"admin's Organization"},"role":"owner"},
  {"id":"2d529b17-24ec-466c-b441-8e905514e0aa","organization":{"id":"8b787d7d-...","name":"demo"},"role":"owner"}
]}

membership 的 id 即创建计时条目时要用的 member_id。

3.4 邀请成员:POST /v1/organizations/{org}/invitations

实测必须带 role(缺省 422:The role field is required.),取值 admin/member 等:

curl -sS -X POST -H "Authorization: Bearer $TOK" -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -d '{"email":"newmember@example.com","role":"admin"}' http://localhost:8137/api/v1/organizations/$ORG/invitations

重复邀请实测返回 400(幂等保护):

{"error":true,"key":"invitation_for_the_email_already_exists","message":"The email has already been invited to the organization..."}

邀请触发队列发信(MAIL_MAILER=log 下邮件全文落在 worker 日志,实测见配置篇 7.1)。

四、项目与任务

4.1 创建项目:POST /v1/organizations/{org}/projects

必填:name、color、is_billable、client_id(可为 null)。

实测踩坑(color 格式):先以 #3B82F6(大写)请求返回 422:

{"message":"The color field must be a valid color.","errors":{"color":["The color field must be a valid color."]}} HTTP=422

查实例内源码(app/Service/ColorService.php)确认校验正则为 ^#[0-9a-f]{6}$——# 后必须 6 位小写十六进制。改 #3b82f6 重试成功:

curl -sS -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{"name":"Website Redesign","color":"#3b82f6","is_billable":true,"client_id":null}' \
  http://localhost:8137/api/v1/organizations/$ORG/projects

实测响应(节选):{"data":{"id":"360eb57c-...","name":"Website Redesign","color":"#3b82f6","is_billable":true}}(下文记 $PID)。

列表形态(GET .../projects):{"data":[...],"links":{...},"meta":{...}}。

4.2 创建任务:POST /v1/organizations/{org}/tasks

必填:name、project_id。

curl -sS -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{"name":"API 实测任务","project_id":"'$PID'"}' http://localhost:8137/api/v1/organizations/$ORG/tasks

实测响应(节选):{"data":{"id":"72cf4be1-...","name":"API 实测任务","project_id":"360eb57c-..."}}(下文记 $TID)。

4.3 删除保护:DELETE /v1/organizations/{org}/tasks/{task}

任务已被计时条目引用时实测删除被拒(引用完整性):

{"error":true,"key":"entity_still_in_use","message":"The task is still used by a time entry and can not be deleted."} HTTP=400

五、计时条目(核心域)

5.1 创建计时:POST /v1/organizations/{org}/time-entries

必填:member_id、start、billable。end 传 null 即运行中条目:

curl -sS -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{"member_id":"2d529b17-...","project_id":"'$PID'","task_id":"'$TID'","start":"2026-10-24T12:35:49Z","end":null,"billable":true,"description":"D5 API 实测条目"}' \
  http://localhost:8137/api/v1/organizations/$ORG/time-entries

实测响应(节选):{"data":{"id":"0fb069b2-...","start":"2026-10-24T12:35:49Z","end":null,"billable":true}}(记 $TE)。可选字段:tags(数组)、type(time/break)。

5.2 查询运行中条目:GET /v1/users/me/time-entries/active

curl -sS -H "Authorization: Bearer $TOK" -H 'Accept: application/json' http://localhost:8137/api/v1/users/me/time-entries/active
# 实测响应(节选):
{"data":{"id":"0fb069b2-442c-463d-96c1-2cb574cb7b7f",...}}

5.3 停止计时:PUT /v1/organizations/{org}/time-entries/{id}

curl -sS -X PUT -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{"start":"2026-10-24T12:35:49Z","end":"2026-10-24T12:36:40Z"}' \
  http://localhost:8137/api/v1/organizations/$ORG/time-entries/$TE
# 实测响应(节选):
{"data":{...,"end":"2026-10-24T12:36:40Z","duration":51}}

duration 由服务端按 start/end 计算(秒)。

实测踩坑(PATCH 是批量语义):集合上的 PATCH /time-entries 并非单条更新,实测必须传 ids + changes;且查源码(TimeEntryUpdateMultipleRequest)确认 changes 仅允许 member_id/project_id/task_id/billable/type/description/tags——不含 start/end,强传实测返回 500 {"message":"Server Error"}。改 start/end 必须走 5.3 的单条 PUT。

5.4 列表与统计

curl -sS -H "Authorization: Bearer $TOK" -H 'Accept: application/json' http://localhost:8137/api/v1/organizations/$ORG/time-entries

实测响应字段(首条):id, start, end, duration, description, task_id, project_id, organization_id, user_id, tags, billable, type。同域还有 aggregate(聚合统计)、export(CSV 导出)、charts/*(仪表盘图表)等只读接口,参数结构见 OpenAPI 规范。

六、实测踩坑速查

#坑现象处置
1全新实例无 Passport 个人客户端createToken 报 Personal access client not found先跑一次 passport:client --personal(2.1 节)
2color 大写十六进制422 must be a valid color# + 6 位小写 hex(源码正则核实)
3PATCH 集合当单条更新用422 要求 ids/changes;强传 start/end 500PATCH=批量改属性;改时间用单条 PUT
4删除被引用的任务400 entity_still_in_use先删/改引用它的计时条目
5邀请缺 role422 The role field is required.显式传 admin/member
6缺 Accept: application/json错误响应可能为 HTML 页统一携带 Accept 头

七、参考链接