OpenHabitTracker 1.2.5 API 使用详解
OpenHabitTracker 1.2.5 API 使用详解:三类路径三种认证通道与 JWT 登录、刷新令牌轮换与 60 秒宽限、DataAccess 九类资源 CRUD 与批量端点、Query 查询域过滤参数、枚举数字与 PascalCase 等通用约定,全部请求与回显实测采集。
OpenHabitTracker 1.2.5 API 使用详解
目标:把自托管 OpenHabitTracker 1.2.5 对外暴露的 HTTP API 讲透——登录认证与刷新令牌轮换、DataAccess 数据域的增删改查与批量端点、Query 查询域的过滤参数、字段命名/枚举/错误响应等通用约定,以及各类实测踩坑。本文所有请求与响应回显均在 Ubuntu 22.04(x86_64)+ OpenHabitTracker 1.2.5 自托管实例(服务端口 8129,演示账号 admin)上逐条实测采集,命令输入与输出逐行照抄真实回显。
一、环境信息
- 操作系统:Ubuntu 22.04.4 LTS(x86_64,PRETTY_NAME 实测输出
Ubuntu 22.04.4 LTS); - OpenHabitTracker 版本:1.2.5(服务端为 .NET 10.0.12 运行时),与《OpenHabitTracker Docker Compose 部署指南》部署的实例一致;
- 服务地址:
http://localhost:8129(HTTP,由 Kestrel 直接提供服务,实测响应头Server: Kestrel); - 数据存储:程序目录下
.OpenHabitTracker/OpenHT.db(SQLite),数据模型见《OpenHabitTracker 1.2.5 配置参数详解》第 5.7 节; - 演示账号:
admin/q1w2e3r4(部署时通过环境变量初始化的管理员); - 客户端工具:curl 7.81.0、python3 3.10(仅用于从登录响应中提取字段)。
二、前置条件
本节交代调用 API 前必须确认的三件事:服务可达、工具就绪、认证通道选对。
2.1 服务端点与客户端工具
确认服务已在 8129 端口监听并响应(首页是 Blazor 页面,未登录访问会 302 跳转登录页,实测状态码):
curl -s -o /dev/null -w '%{http_code}' http://localhost:8129/
302
curl --version | head -1
curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2 zlib/1.2.11 brotli/1.0.9 zstd/1.4.8 libidn2/2.3.2 libpsl/0.21.0 (+libidn2/2.3.2) libssh/0.9.6/openssl/zlib nghttp2/1.43.0 librtmp/2.3 OpenLDAP/2.5.16
💡 说明:API 全部为 HTTP + JSON,任何能发 HTTP 请求的工具(curl、HTTPie、Postman、各类语言 HTTP 库)均可调用。本文统一用 curl 演示,JSON 字段提取用 python3 一行命令完成,避免额外安装 jq。
2.2 三类路径、三种认证通道(必须先分清)
OpenHabitTracker 1.2.5 的认证不是一刀切,实测三类路径对应三种认证方式,选错通道直接 401:
| 路径 | 认证方式 | 说明 |
|---|---|---|
POST /api/Auth/jwt-token、POST /api/Auth/refresh-token | 匿名 | 登录与刷新令牌,无需任何凭据头 |
/api/DataAccess/*、/api/Query/* | JWT Bearer(必须) | 请求头带 Authorization: Bearer <JwtToken>,实测缺失/错误令牌均 401 |
GET /api/Auth/current-user、/openapi/v1.json、/scalar/v1 | 登录 Cookie | 走浏览器登录态(.AspNetCore.Identity.Application Cookie),实测携带 JWT 反而 401 |
⚠️ 注意:
current-user、/openapi/v1.json、/scalar/v1这三处用 JWT 会得到 401——它们的鉴权策略走默认 Cookie 方案,而数据接口控制器显式声明了 JWT Bearer 方案。这是 1.2.5 源码中两类控制器[Authorize]特性写法不同导致的真实行为(见第 4.3 节实测与第八章 Q1/Q5)。
三、API 总览
本节给出 API 的整体版图:协议与文档入口、端点清单、命名约定与限流约定,先建立全局认知再进入逐域详解。
3.1 协议与 OpenAPI 文档
- 协议:HTTP/1.1 + JSON(Kestrel 直接暴露,未强制 HTTPS;生产建议置于反向代理之后启用 TLS);
- OpenAPI 文档:
/openapi/v1.json,OpenAPI 3.1.1,文档标题OpenHT | v1,共 34 个路径、73 个操作,实测带登录 Cookie 拉取:
curl -s http://localhost:8129/openapi/v1.json -b cookies.txt | head -c 120
{
"openapi": "3.1.1",
"info": {
"title": "OpenHT | v1",
"version": "1.0.0"
},
"servers": [
{
"
(输出节选,实测首尾;完整文档约 120 KB)
- 交互式文档:Scalar UI,地址
/scalar/v1,浏览器登录后可直接在页面上试调所有端点(实测未登录 401、带登录 Cookie 200)。
3.2 端点清单(按域分组)
| 域 | 路径前缀 | 路径数 | 端点构成 |
|---|---|---|---|
| Auth 认证 | /api/Auth/ | 3 | POST jwt-token、POST refresh-token、GET current-user |
| DataAccess 数据 | /api/DataAccess/ | 28 | 9 类资源(users/habits/notes/tasks/times/items/categories/priorities/settings)的单建/批量建/列表/单查/更新/单删/批量删,另加 DELETE delete-user-data 一键清空 |
| Query 查询 | /api/Query/ | 3 | habits、notes、tasks 各提供 POST(参数在请求体)与 GET(参数在查询串)两种形态 |
9 类资源中,habits/notes/tasks/times/items/categories 是日常业务资源,priorities/settings/users 偏系统资源,端点形态完全一致(详见第 5.1 节的模式表),学会一个资源等于学会九个。
3.3 命名与格式约定(实测归纳)
- 请求体字段大小写不敏感:
{"title":"..."}与{"Title":"..."}都能正确绑定(本文实测统一用小写); - 响应体字段固定 PascalCase(如
"Title"、"RepeatCount")——控制器关闭了 camelCase 改写,客户端映射时注意(见第八章 Q8); - 枚举只接受数字:请求体里
priority、repeatPeriod等枚举字段必须传 JSON 数字,传字符串名(如"Medium")直接 400(实测见第八章 Q2),枚举值对照表见第 7.2 节; - 主键为 int64:新建资源返回自增 Id;时间格式 ISO 8601(如
"2026-10-25T09:00:00"),时长用"01:00:00"形式; - 无分页:所有列表接口全量返回,无 page/size 参数(见第 7.4 节);
- 无内置限流:1.2.5 源码未注册任何限流中间件,公网部署需自行在反向代理层限流。
四、认证与首次调用
本节从登录开始,完整走通「拿令牌 → 带令牌调接口 → 刷新令牌 → 各类认证失败形态」,所有回显为真实响应。
4.1 登录获取 JWT
POST /api/Auth/jwt-token 请求体为 LoginCredentials(必填 username、password)。实测(-D - 把响应头打到终端,响应体写入 login.json):
curl -s -D - -X POST http://localhost:8129/api/Auth/jwt-token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}' \
-o login.json
实测响应头(节选):
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel
Set-Cookie: .AspNetCore.Identity.Application=CfDJ8PKo8KDQ61ZBny2qnYTKG0N6E-hQMe9iZUQeZAiqRoVtMPYxE-9
⚠️ 输出节选(实测首尾):
Set-Cookie为 ASP.NET Core Identity 登录 Cookie(HttpOnly,截取前 100 字符)。登录接口同时完成两件事:签发 JWT(给 API 调用方)和种下浏览器登录 Cookie(给 current-user/OpenAPI 文档用),两者用途不同,见 2.2 节通道表。
响应体固定三个字段(PascalCase,实测输出):
python3 -c "import json;d=json.load(open('login.json'));print(sorted(d))"
['JwtToken', 'JwtTokenExpiresAt', 'RefreshToken']
提取 JWT 存入 token.txt 供后续请求复用,并确认长度:
python3 -c 'import json;open("token.txt","w").write(json.load(open("login.json"))["JwtToken"])'
TOKEN=$(cat token.txt); echo "${#TOKEN}"
272
💡 说明:令牌为 HS256 签名的 JWT,本例实测 272 字符,首尾节选
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ... Q0lgQzWyuMKsrOWo(每次登录值都不同)。JwtTokenExpiresAt实测为登录时刻 +1 天(本例2026-10-25T06:54:49.2853813+00:00)。
解码令牌载荷可看到签发方、受众与过期时间(实测):
python3 -c "import json,base64;p=json.load(open('login.json'))['JwtToken'].split('.')[1];print(json.dumps(json.loads(base64.urlsafe_b64decode(p+'==')),ensure_ascii=False))"
{"sub": "admin", "jti": "699bff0a-21ac-44f7-b5a2-38df0e91f18c", "exp": 1792911289, "iss": "https://app.openhabittracker.net", "aud": "OpenHabitTracker"}
三个字段含义:sub 为登录用户名,exp 为 Unix 时间戳(与 JwtTokenExpiresAt 一致),iss/aud 固定为官方约定值,服务端按同样的 issuer/audience 校验,因此 JwtSecret 更换后旧令牌全部失效(详见《OpenHabitTracker 1.2.5 配置参数详解》第 5.2 节)。
4.2 携带 JWT 调用数据接口
/api/DataAccess/* 与 /api/Query/* 全部要求 Authorization: Bearer <JwtToken>。以读习惯列表为例(此时尚无数据,返回空数组 []):
curl -s http://localhost:8129/api/DataAccess/habits -H "Authorization: Bearer $TOKEN"
[]
💡 技术拓展:HTTP API 凭据传递常见三种方式对比——
方式 原理 适用场景 Bearer 令牌 每次请求带 Authorization头,无状态脚本、移动端、跨服务调用 会话 Cookie 服务端会话 + 浏览器自动携带 浏览器网页应用 API Key 静态长密钥,常放请求头或查询串 服务器间长期集成 OpenHabitTracker 1.2.5 实际同时支持前两种:数据接口走 Bearer JWT,浏览器页面走 Identity Cookie,未提供静态 API Key。
4.3 current-user:Bearer 与 Cookie 的实测差异
GET /api/Auth/current-user 用于确认当前登录身份。用 JWT 访问返回 401(实测完整响应):
curl -s -i http://localhost:8129/api/Auth/current-user -H "Authorization: Bearer $TOKEN"
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Sat, 24 Oct 2026 06:47:05 GMT
Server: Kestrel
WWW-Authenticate: Bearer
换成登录时种下的 Cookie(-b 携带 cookies.txt)则返回当前用户(实测):
curl -s http://localhost:8129/api/Auth/current-user -b cookies.txt
{"Id":0,"UserName":"admin","Email":"admin@admin.com","PasswordHash":"","LastChangeAt":"0001-01-01T00:00:00"}
原因:该端点所在控制器未显式指定认证方案,落到默认 Cookie 方案上;而数据接口控制器显式声明了 JwtBearer。给脚本化调用方一个稳妥结论——判断 JWT 是否有效,用任意数据接口(如 GET /api/DataAccess/habits)探测即可,不要用 current-user。
4.4 刷新令牌:轮换与 60 秒宽限
JWT 1 天过期后无需重新输密码,用 RefreshToken(登录响应中返回,实测 88 字符 Base64,有效期 90 天)调 POST /api/Auth/refresh-token 换新令牌。实测(请求体 {"refreshToken":"..."}):
curl -s -X POST http://localhost:8129/api/Auth/refresh-token \
-H 'Content-Type: application/json' \
-d "{\"refreshToken\":\"$REFRESH\"}" -o rt.json -w '%{http_code}'
200
python3 -c "import json;d=json.load(open('rt.json'));print(sorted(d));print('RefreshToken 已轮换:',d['RefreshToken'][:12],'... len',len(d['RefreshToken']))"
['JwtToken', 'JwtTokenExpiresAt', 'RefreshToken']
RefreshToken 已轮换: Ry2P7xDi6Pbn ... len 88
刷新是轮换制:返回全新的 JwtToken 与 RefreshToken,旧 RefreshToken 进入 60 秒宽限期(防止网络丢包导致客户端永远掉线)。宽限期内重放旧令牌实测返回 200,且返回的就是当前那个新令牌(不会再次轮换):
# 登录得 RT0 → 立即用 RT0 刷新得 RT1 → 数秒内再次重放 RT0
curl -s -X POST http://localhost:8129/api/Auth/refresh-token \
-H 'Content-Type: application/json' -d "{\"refreshToken\":\"$RT0\"}" -o rt4.json -w '%{http_code}'
200
python3 -c "import json;print(json.load(open('rt3.json'))['RefreshToken']==json.load(open('rt4.json'))['RefreshToken'])"
True
超过 60 秒宽限后重放旧令牌,返回 401 且服务端删除该令牌记录,本设备后续只能重新密码登录(实测):
curl -s -X POST http://localhost:8129/api/Auth/refresh-token \
-H 'Content-Type: application/json' -d "{\"refreshToken\":\"$RTOLD\"}" -w ' HTTP %{http_code}'
Invalid or expired refresh token HTTP 401
4.5 认证失败形态汇总(实测)
| 场景 | 实测响应 |
|---|---|
| 数据接口不带令牌 | 401 空响应体,响应头 WWW-Authenticate: Bearer |
数据接口带伪造令牌(abc.def.ghi) | 401 空响应体,响应头 WWW-Authenticate: Bearer error="invalid_token" |
| 登录口令错误 | 401 纯文本响应体:Invalid credentials |
| 刷新令牌无效/过期/重放超宽限 | 401 纯文本响应体:Invalid or expired refresh token |
坏令牌实测完整回显(对照排查时可直接比对):
curl -s -i http://localhost:8129/api/DataAccess/habits -H 'Authorization: Bearer abc.def.ghi'
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Sat, 24 Oct 2026 06:47:05 GMT
Server: Kestrel
WWW-Authenticate: Bearer error="invalid_token"
口令错误实测(注意响应体是纯文本,不是 JSON):
curl -s -X POST http://localhost:8129/api/Auth/jwt-token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"wrong-pass"}' -w ' HTTP %{http_code}'
Invalid credentials HTTP 401
五、DataAccess 数据域详解
DataAccess 域承担全部业务数据的增删改查,28 个路径对 9 类资源呈完全一致的模式。本节按「模式总览 → 分类 → 习惯 → 打卡 → 笔记 → 任务与清单项 → 用户 → 一键清空」顺序逐端点实测。
5.1 端点模式总览
9 类资源共用同一套端点模式,以 habits 为例(其余资源把 habits 替换成 notes/tasks/times/items/categories/priorities/settings/users 即可):
| 方法与路径 | 请求体 | 成功响应 | 语义 |
|---|---|---|---|
POST /api/DataAccess/habit(单数) | 单个实体 JSON | 新建 Id(纯数字) | 单建 |
POST /api/DataAccess/habits(复数) | 实体 JSON 数组 | Id 数组(如 [23,24]) | 批量建 |
GET /api/DataAccess/habits | 无 | 实体数组 | 列表(全量) |
GET /api/DataAccess/habits/{id} | 无 | 单个实体 JSON(不存在则 404) | 单查 |
PUT /api/DataAccess/habits(仅复数路径) | 单个实体 JSON(必须含 Id) | 200 空体 | 更新 |
DELETE /api/DataAccess/habits/{id} | 无 | 200 空体 | 单删 |
DELETE /api/DataAccess/habits(复数路径) | Id 数组(如 [23]) | 200 空体 | 批量删 |
⚠️ 注意:单数/复数路径的请求体形态是硬约束——
PUT只在复数路径上但收单个实体,POST复数路径只收数组,传反了都是 400(实测回显见第八章 Q3/Q4)。
times 与 items 的列表接口各带一个可选过滤参数:GET /api/DataAccess/times?habitId=<id> 按习惯过滤打卡,GET /api/DataAccess/items?parentId=<id> 按父任务过滤清单项。
5.2 分类(categories)
习惯和任务可归属分类。先建一个分类(返回新 Id):
curl -s -X POST http://localhost:8129/api/DataAccess/category \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"健康","color":"#4CAF50"}'
5
列表回显(UserId 为归属用户,实测返回 0 表示当前用户资源未显式落用户列):
curl -s http://localhost:8129/api/DataAccess/categories -H "Authorization: Bearer $TOKEN"
[{"Id":5,"UserId":0,"Title":"健康","IsCollapsed":false,"CompletionRule":0}]
5.3 习惯(habits):完整 CRUD
单建。请求体为 HabitEntity,枚举字段全部传数字(priority:3=Medium、repeatPeriod:0=Day、displayMetric:2=Quantity,对照表见 7.2 节),categoryId 关联上一步分类:
curl -s -X POST http://localhost:8129/api/DataAccess/habit \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"晨间跑步","color":"#4CAF50","priority":3,"repeatCount":1,"repeatInterval":1,"repeatPeriod":0,"displayMetric":2,"targetQuantity":3,"categoryId":5}'
20
列表(与单查 /api/DataAccess/habits/20 返回结构一致,实测):
curl -s http://localhost:8129/api/DataAccess/habits -H "Authorization: Bearer $TOKEN"
[{"RepeatCount":1,"RepeatInterval":1,"RepeatPeriod":0,"DisplayMetric":2,"TargetQuantity":3,"Duration":null,"LastTimeDoneAt":null,"StartAt":null,"Id":20,"CategoryId":5,"Title":"晨间跑步","Color":"#4CAF50","Priority":3,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"0001-01-01T00:00:00"}]
⚠️ 注意新建实体的
CreatedAt/UpdatedAt为0001-01-01T00:00:00——1.2.5 通过 API 新建时未回填创建时间,属于真实默认行为(见第八章 Q7)。
更新。标准做法是先 GET 单个实体、改字段、PUT 回去(请求体必须携带完整实体含 Id)。实测取出习惯 Id=24、把标题改为「冥想十分钟(已改)」后 PUT:
curl -s http://localhost:8129/api/DataAccess/habits/24 -o habit-main.json
# 修改 habit-main.json 中的 Title 字段后保存为 habit-put-main.json
python3 -c "import json;print(json.load(open('habit-put-main.json')))"
{"RepeatCount": 0, "RepeatInterval": 0, "RepeatPeriod": 0, "DisplayMetric": 0, "TargetQuantity": 0, "Duration": null, "LastTimeDoneAt": null, "StartAt": null, "Id": 24, "CategoryId": 0, "Title": "冥想十分钟(已改)", "Color": "#009688", "Priority": 2, "IsDeleted": false, "CreatedAt": "0001-01-01T00:00:00", "UpdatedAt": "0001-01-01T00:00:00"}
curl -s -o /dev/null -w '%{http_code}' -X PUT http://localhost:8129/api/DataAccess/habits \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
--data-binary @habit-put-main.json
200
PUT 成功响应为 200 空体。GET 验证生效(UpdatedAt 已变为服务器本地时间,本实例时区 TZ=Asia/Shanghai):
curl -s http://localhost:8129/api/DataAccess/habits/24 -H "Authorization: Bearer $TOKEN"
{"RepeatCount":0,"RepeatInterval":0,"RepeatPeriod":0,"DisplayMetric":0,"TargetQuantity":0,"Duration":null,"LastTimeDoneAt":null,"StartAt":null,"Id":24,"CategoryId":0,"Title":"冥想十分钟(已改)","Color":"#009688","Priority":2,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"2026-10-24T14:54:49.8001503"}
批量建。请求体为数组,返回 Id 数组(实测一次建两个习惯):
curl -s -X POST http://localhost:8129/api/DataAccess/habits \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '[{"title":"睡前阅读","color":"#3F51B5","priority":1},{"title":"冥想十分钟","color":"#009688","priority":2}]'
[23,24]
单删与批量删。均返回 200 空体;删除后列表不再包含该行(实测):
curl -s -X DELETE http://localhost:8129/api/DataAccess/habits/24 -H "Authorization: Bearer $TOKEN" -w ' HTTP %{http_code}'
HTTP 200
curl -s http://localhost:8129/api/DataAccess/habits -H "Authorization: Bearer $TOKEN" | python3 -c 'import json,sys;print([(x["Id"],x["Title"]) for x in json.load(sys.stdin)])'
[(20, '晨间跑步'), (23, '睡前阅读')]
curl -s -X DELETE http://localhost:8129/api/DataAccess/habits \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '[23]' -w ' HTTP %{http_code}'
HTTP 200
5.4 打卡(times)
习惯的每次执行记录一条 time,quantity 记完成量(与习惯的 targetQuantity 对应),startedAt 记开始时间。实测创建(返回新 Id=3)并按习惯过滤查询:
curl -s -X POST http://localhost:8129/api/DataAccess/time \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"habitId":20,"startedAt":"2026-10-24T07:30:00","quantity":3}'
3
curl -s 'http://localhost:8129/api/DataAccess/times?habitId=20' -H "Authorization: Bearer $TOKEN"
[{"Id":3,"HabitId":20,"StartedAt":"2026-10-24T07:30:00","CompletedAt":null,"Quantity":3}]
💡 说明:
startedAt不传时服务端落0001-01-01T00:00:00默认值;查询域返回的习惯数据会把打卡聚合进TimesDone数组(见 6.2 节),这正是应用内「overdue 进度条」的数据来源。
5.5 笔记(notes)
笔记正文 content 支持 Markdown 文本。实测创建与列表:
curl -s -X POST http://localhost:8129/api/DataAccess/note \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"欢迎笔记","content":"这是用 API 创建的第一条 Markdown 笔记"}'
21
curl -s http://localhost:8129/api/DataAccess/notes -H "Authorization: Bearer $TOKEN"
[{"Content":"这是用 API 创建的第一条 Markdown 笔记","Id":21,"CategoryId":0,"Title":"欢迎笔记","Color":"bg-body-secondary","Priority":0,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"0001-01-01T00:00:00"}]
5.6 任务(tasks)与清单项(items)
任务支持计划时间 plannedAt、时长 duration(TimeSpan 格式)、优先级 priority。清单项(item)是任务下的检查子项,通过 parentId 挂到任务上。实测创建任务(返回 Id=22)与清单项(返回 Id=3):
curl -s -X POST http://localhost:8129/api/DataAccess/task \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"写周报","priority":4,"plannedAt":"2026-10-25T09:00:00","duration":"01:00:00"}'
22
curl -s -X POST http://localhost:8129/api/DataAccess/item \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"parentId":22,"title":"收集本周数据"}'
3
列表与按父过滤(注意 tasks 列表不含清单项,items 用 parentId 过滤查询,实测):
curl -s http://localhost:8129/api/DataAccess/tasks -H "Authorization: Bearer $TOKEN"
[{"PlannedAt":"2026-10-25T09:00:00","Duration":"01:00:00","StartedAt":null,"CompletedAt":null,"Id":22,"CategoryId":0,"Title":"写周报","Color":"bg-body-secondary","Priority":4,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"0001-01-01T00:00:00"}]
curl -s 'http://localhost:8129/api/DataAccess/items?parentId=22' -H "Authorization: Bearer $TOKEN"
[{"Id":3,"ParentId":22,"Title":"收集本周数据","DoneAt":null}]
任务更新与习惯同模式(GET 单个 → 改字段 → PUT)。实测把任务标题改为「写周报(已改)」(本例取自另一轮实测,任务 Id=17):
curl -s -o /dev/null -w '%{http_code}' -X PUT http://localhost:8129/api/DataAccess/tasks \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
--data-binary @task-put.json
200
改后用查询域按关键词验证(实测命中新标题,清单项仍随 Items 返回):
curl -s -G http://localhost:8129/api/Query/tasks -H "Authorization: Bearer $TOKEN" --data-urlencode 'searchTerm=已改'
[{"PlannedAt":"2026-10-25T09:00:00","StartedAt":null,"CompletedAt":null,"Items":[{"Title":"收集本周数据","DoneAt":null}],"Duration":"01:00:00","Title":"写周报(已改)","Color":"bg-body-secondary","Priority":4,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"2026-10-24T12:43:03.2044855"}]
5.7 用户(users)
GET /api/DataAccess/users 返回用户列表,实测包含 PasswordHash 字段(ASP.NET Core Identity 哈希,非明文):
curl -s http://localhost:8129/api/DataAccess/users -H "Authorization: Bearer $TOKEN"
[{"Id":1,"UserName":"admin","Email":"admin@admin.com","PasswordHash":"AQAAAAIAAYagAAAAELYqY4joNeGZpEUH59u57oc6OY/EYvaRqxn5fjpBMMeI64KyfPiOuA39L6lPf3Bm8A==","LastChangeAt":"2026-10-24T06:54:49.9112592"}]
⚠️ 注意:该接口会返回口令哈希列,虽为单向哈希仍不建议暴露给不可信网络。生产部署请把服务置于反向代理之后并收紧访问控制(口令哈希算法与账号体系见《OpenHabitTracker 1.2.5 配置参数详解》第 5.1 节)。
5.8 一键清空(delete-user-data)
DELETE /api/DataAccess/delete-user-data 一次性删除当前登录用户名下的全部业务数据(习惯/打卡/笔记/任务/清单项/分类等),实测返回 200 空体;随后逐列表验证全部为空数组(实测):
curl -s -X DELETE http://localhost:8129/api/DataAccess/delete-user-data -H "Authorization: Bearer $TOKEN" -w ' HTTP %{http_code}'
HTTP 200
curl -s http://localhost:8129/api/DataAccess/habits -H "Authorization: Bearer $TOKEN"
[]
curl -s http://localhost:8129/api/DataAccess/categories -H "Authorization: Bearer $TOKEN"
[]
curl -s http://localhost:8129/api/DataAccess/notes -H "Authorization: Bearer $TOKEN"
[]
curl -s http://localhost:8129/api/DataAccess/tasks -H "Authorization: Bearer $TOKEN"
[]
⚠️ 注意:该端点不可逆且无需二次确认,只清业务数据、保留登录账号(清空后用同一账号仍可正常登录并继续建数据,实测验证过)。调试脚本务必确认 TOKEN 指向的账号再调用。
六、Query 查询域详解
DataAccess 域只有「全量列表」,过滤、搜索与聚合展示由 Query 域承担:habits/notes/tasks 各提供 POST(QueryParameters 放请求体)与 GET(QueryParameters 放查询串)两种等价形态,响应模型比实体多了聚合字段(习惯带 TimesDone 打卡聚合,任务带 Items 清单项)。
6.1 两种调用形态与 QueryParameters 参数
POST 形态把参数放 JSON 请求体,GET 形态把参数放 URL 查询串(中文等特殊字符建议 --data-urlencode)。QueryParameters 常用参数(依据 1.2.5 OpenAPI schema 实查):
| 参数 | 类型 | 说明 |
|---|---|---|
searchTerm | string | 关键词搜索(标题/内容匹配) |
matchCase | boolean | 搜索是否区分大小写 |
hideCompletedTasks | boolean | 隐藏已完成任务 |
doneAtFilter + doneAtCompare | string + 枚举 | 按完成时间过滤,Compare 取值 Before/On/After/NotOn(数字 0-3) |
plannedAtFilter + plannedAtCompare | string + 枚举 | 按计划时间过滤,取值同上 |
plannedRangeStart / plannedRangeEnd | string | 计划时间区间 |
doneRangeStart / doneRangeEnd + showDoneInRange | string + boolean | 完成时间区间 |
selectedCategoryId / hiddenCategoryIds | integer / 数组 | 按分类过滤 |
selectedPriority | integer | 按优先级过滤 |
showOnlyOverSelectedRatioMin / showOnlyUnderSelectedRatioMax + selectedRatioMin/Max | boolean + integer | 按完成比例(overdue 模型)筛选,比例口径由 selectedRatio 指定(ElapsedToAverage/ElapsedToDesired/AverageToDesired) |
sortBy | object | 排序设置(与应用内排序面板字段对应) |
6.2 习惯查询(含打卡聚合)
POST 形态按关键词「跑步」搜索,响应带 TimesDone 聚合(与 5.4 节创建的打卡记录关联,实测):
curl -s -X POST http://localhost:8129/api/Query/habits \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"searchTerm":"跑步"}'
[{"RepeatCount":1,"RepeatInterval":1,"RepeatPeriod":0,"DisplayMetric":2,"TargetQuantity":3,"LastTimeDoneAt":null,"StartAt":null,"TimesDone":[{"StartedAt":"2026-10-24T07:30:00","CompletedAt":null,"Quantity":3}],"Items":[],"Duration":null,"Title":"晨间跑步","Color":"#4CAF50","Priority":3,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"0001-01-01T00:00:00"}]
6.3 任务查询(含清单项聚合)
GET 形态按关键词「周报」搜索(实测命中 5.6 节创建的任务,Items 聚合了清单项):
curl -s -G http://localhost:8129/api/Query/tasks -H "Authorization: Bearer $TOKEN" --data-urlencode 'searchTerm=周报'
[{"PlannedAt":"2026-10-25T09:00:00","StartedAt":null,"CompletedAt":null,"Items":[{"Title":"收集本周数据","DoneAt":null}],"Duration":"01:00:00","Title":"写周报","Color":"bg-body-secondary","Priority":4,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"0001-01-01T00:00:00"}]
6.4 笔记查询
GET 形态按关键词「Markdown」搜索(实测命中 5.5 节创建的笔记,响应含 Content 全文):
curl -s -G http://localhost:8129/api/Query/notes -H "Authorization: Bearer $TOKEN" --data-urlencode 'searchTerm=Markdown'
[{"Content":"这是用 API 创建的第一条 Markdown 笔记","Title":"欢迎笔记","Color":"bg-body-secondary","Priority":0,"IsDeleted":false,"CreatedAt":"0001-01-01T00:00:00","UpdatedAt":"0001-01-01T00:00:00"}]
七、通用约定与错误码
本节把散落在各节的格式约定集中成表,便于客户端开发时速查。
7.1 字段命名与大小写
请求体字段大小写不敏感(推荐小写驼峰),响应体固定 PascalCase。给客户端的实操建议:序列化请求时用小写驼峰、反序列化响应时按 PascalCase 映射(或解析时统一做大小写不敏感匹配)。
7.2 枚举值对照表(请求传数字)
| 枚举 | 取值(数字 = 名称) | 用于 |
|---|---|---|
| Priority | 0=None, 1=VeryLow, 2=Low, 3=Medium, 4=High, 5=VeryHigh | habits/notes/tasks 的 priority |
| Period | 0=Day, 1=Week, 2=Month, 3=Year | 习惯重复周期 repeatPeriod |
| DisplayMetric | 0=Repetitions, 1=Time, 2=Quantity | 习惯计量方式 displayMetric |
| DateCompare | 0=Before, 1=On, 2=After, 3=NotOn | Query 域时间比较 doneAtCompare/plannedAtCompare |
| Ratio | 0=ElapsedToAverage, 1=ElapsedToDesired, 2=AverageToDesired | Query 域完成比例口径 selectedRatio |
| FilterDisplay | 0=CheckBoxes, 1=RadioButtons, 2=SelectOptions | 设置中过滤器展示形态 |
⚠️ 注意:枚举传字符串名(
"Medium"、"Day")会得到 400 校验错误,因为 API 控制器的 JSON 配置未启用枚举字符串转换(实测回显与定位见第八章 Q2)。上表数字与名称的对应关系已在 OpenAPI schema 中实查核对(如priority:3实测落库后 GET 返回"Priority":3,应用内显示 Medium)。
7.3 时间与时长格式
- 时间字段(
startedAt、plannedAt、各类 Filter):ISO 8601,如"2026-10-24T07:30:00"、"2026-10-25T09:00:00",不带时区时按服务器本地时区解释(本实例 TZ=Asia/Shanghai); - 时长字段(
duration):"01:00:00"的 TimeSpan 格式; - 响应中的
UpdatedAt为服务器本地时间(如"2026-10-24T14:54:49.8001503");登录响应的JwtTokenExpiresAt为带时区的 UTC 时间(2026-10-25T06:54:49.2853813+00:00)。
7.4 分页、过滤与排序
- 分页:不存在。全部列表接口(DataAccess 与 Query 域)一次返回全量数据,无 page/limit 参数;数据量大时靠 Query 域的条件过滤收敛结果集;
- 过滤:DataAccess 域仅
times?habitId=与items?parentId=两个过滤参数;通用条件过滤(关键词/时间区间/分类/优先级/完成比例)统一在 Query 域的 QueryParameters; - 排序:列表接口不接受排序参数,排序逻辑在客户端;QueryParameters 的
sortBy对应应用内排序面板的设置项。
7.5 错误响应形态(实测三类)
| 状态码 | 形态 | 实测样例 |
|---|---|---|
| 400 | ProblemDetails JSON(application/problem+json) | 枚举传字符串(Q2)、复数端点传错体形态(Q3/Q4)、PUT 缺 Id |
| 401 | 空响应体(数据接口)或纯文本(登录/刷新) | 见 4.5 节汇总表 |
| 404 | ProblemDetails JSON | GET /api/DataAccess/habits/999999 |
404 实测完整回显:
curl -s -i http://localhost:8129/api/DataAccess/habits/999999 -H "Authorization: Bearer $TOKEN"
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
Date: Sat, 24 Oct 2026 06:49:07 GMT
Server: Kestrel
Transfer-Encoding: chunked
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"traceId":"00-3c5d14cd00b500a0c0526eb8a17f515b-2ad8047b0c351889-00"}
💡 技术拓展:ProblemDetails(RFC 9457,旧称 RFC 7807)是 HTTP API 机器可读错误的标准格式,常见实现对比——
实现 特点 适用场景 ASP.NET Core ApiController 自动校验 框架内建, errors按字段聚合.NET Web API(本文即此形态) 自定义错误 DTO 字段自由但格式不统一 遗留系统 异常中间件统一包装 全局兜底,可带堆栈 5xx 场景(OpenHabitTracker 对 /api/ 下的 500 会返回含 message/stackTrace 的 JSON) OpenHabitTracker 1.2.5 的 400/404 走 ASP.NET Core 自动 ProblemDetails,排障时
errors键里的$.字段名直接指出非法字段,traceId可与日志对账。
八、常见问题(FAQ)
以下问题均来自实测踩坑,回显逐行照抄。
Q1:用 JWT 调 current-user 得到 401,令牌明明是刚登录拿到的?
current-user 走 Cookie 认证通道,JWT 只对 /api/DataAccess/* 与 /api/Query/* 生效(通道划分见 2.2 节表,实测见 4.3 节)。脚本里想验证令牌有效性,改用 GET /api/DataAccess/habits 探测。
Q2:习惯创建一直 400,报 The JSON value could not be converted to OpenHabitTracker.Data.Priority?
枚举字段传了字符串。API 控制器只接受数字枚举,把 "priority":"Medium" 改成 "priority":3、"repeatPeriod":"Day" 改成 "repeatPeriod":0 即可。实测错误回显:
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"habit":["The habit field is required."],"$.priority":["The JSON value could not be converted to OpenHabitTracker.Data.Priority. Path: $.priority | LineNumber: 0 | BytePositionInLine: 52."]},"traceId":"00-126ff1afe8112b099e83c793790b828b-cc918b960d405c75-00"}
(errors 里的 $.priority 即非法字段路径;habit 报 required 是同一请求体绑定失败的连带提示。)
Q3:PUT /api/DataAccess/habits 传数组报 400?
更新端点只收单个实体,收数组会报 The JSON value could not be converted to OpenHabitTracker.Data.Entities.HabitEntity(实测)。批量更新请逐个实体循环调用 PUT。
Q4:POST /api/DataAccess/habits(复数)传单个对象报 400?
复数端点只收数组,报 The JSON value could not be converted to System.Collections.Generic.IReadOnlyList`1[OpenHabitTracker.Data.Entities.HabitEntity](实测)。单建请用单数路径 POST /api/DataAccess/habit。
Q5:浏览器外用 JWT 拉 /openapi/v1.json 或访问 /scalar/v1 得到 401?
这两个入口与 current-user 一样走 Cookie 认证策略。先在浏览器登录应用(/login),同一会话再访问即可;脚本拉取时可先调登录接口把 Cookie 存下来(-c cookies.txt),再用 -b cookies.txt 访问(实测 401 → 带 Cookie 200)。这部分与《OpenHabitTracker Docker Compose 部署指南》第十一章 Q4 的结论一致。
Q6:刷新令牌重放为什么有时 200 有时 401? 轮换后的旧 RefreshToken 有 60 秒宽限期:期内重放返回 200 且发还当前新令牌(实测验证返回值与新令牌一致),超过 60 秒重放返回 401 并删除令牌记录,该设备只能重新密码登录(实测回显见 4.4 节)。多个客户端共用同一账号时不要共享 RefreshToken,各自登录各持一份。
Q7:新建资源的 CreatedAt 为什么是 0001-01-01T00:00:00?
1.2.5 通过 API 新建时未回填创建/更新时间,是真实默认行为而非故障;执行过 PUT 更新的资源 UpdatedAt 会落为服务器本地时间(实测见 5.3 节)。需要时间戳的业务请以自己的 startedAt/plannedAt 或打卡记录为准。
Q8:客户端按 camelCase 解析响应拿到全 null?
响应字段是 PascalCase("Title" 而非 "title",实测贯穿全文)。反序列化时开启大小写不敏感匹配,或按 PascalCase 声明字段。请求方向不受影响(大小写不敏感)。
Q9:delete-user-data 之后还能登录吗?
能。该端点只清当前用户的业务数据(习惯/打卡/笔记/任务/清单项/分类等),登录账号与口令保留——实测清空后同一账号立即重新登录、建数据、再清空,全部正常(第 5.8 节即第二轮完整实测)。
九、参考链接
- OpenHabitTracker 官网:https://www.openhabittracker.net/
- GitHub 仓库(Jinjinov/OpenHabitTracker):https://github.com/Jinjinov/OpenHabitTracker
- 1.2.5 认证控制器源码 AuthController.cs:https://github.com/Jinjinov/OpenHabitTracker/blob/1.2.5/OpenHabitTracker.Blazor.Web/Controllers/AuthController.cs
- 1.2.5 数据控制器源码 DataAccessController.cs:https://github.com/Jinjinov/OpenHabitTracker/blob/1.2.5/OpenHabitTracker.Blazor.Web/Controllers/DataAccessController.cs
- 1.2.5 查询控制器源码 QueryController.cs:https://github.com/Jinjinov/OpenHabitTracker/blob/1.2.5/OpenHabitTracker.Blazor.Web/Controllers/QueryController.cs
- 自托管实例内 OpenAPI 文档:http://localhost:8129/openapi/v1.json(需登录 Cookie)
- 自托管实例内 Scalar 交互文档:http://localhost:8129/scalar/v1(需登录 Cookie)
- 本专栏相关文档:《OpenHabitTracker 1.2.5 介绍与选型指南》、《OpenHabitTracker Docker Compose 部署指南》、《OpenHabitTracker 1.2.5 配置参数详解》