OpenHabitTracker 1.2.5 API 使用详解

OpenHabitTracker 1.2.5 API 使用详解:三类路径三种认证通道与 JWT 登录、刷新令牌轮换与 60 秒宽限、DataAccess 九类资源 CRUD 与批量端点、Query 查询域过滤参数、枚举数字与 PascalCase 等通用约定,全部请求与回显实测采集。

OpenHabitTracker习惯追踪自托管2026/10/24

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/3POST jwt-token、POST refresh-token、GET current-user
DataAccess 数据/api/DataAccess/289 类资源(users/habits/notes/tasks/times/items/categories/priorities/settings)的单建/批量建/列表/单查/更新/单删/批量删,另加 DELETE delete-user-data 一键清空
Query 查询/api/Query/3habits、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。

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 实查):

参数类型说明
searchTermstring关键词搜索(标题/内容匹配)
matchCaseboolean搜索是否区分大小写
hideCompletedTasksboolean隐藏已完成任务
doneAtFilter + doneAtComparestring + 枚举按完成时间过滤,Compare 取值 Before/On/After/NotOn(数字 0-3)
plannedAtFilter + plannedAtComparestring + 枚举按计划时间过滤,取值同上
plannedRangeStart / plannedRangeEndstring计划时间区间
doneRangeStart / doneRangeEnd + showDoneInRangestring + boolean完成时间区间
selectedCategoryId / hiddenCategoryIdsinteger / 数组按分类过滤
selectedPriorityinteger按优先级过滤
showOnlyOverSelectedRatioMin / showOnlyUnderSelectedRatioMax + selectedRatioMin/Maxboolean + integer按完成比例(overdue 模型)筛选,比例口径由 selectedRatio 指定(ElapsedToAverage/ElapsedToDesired/AverageToDesired)
sortByobject排序设置(与应用内排序面板字段对应)

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 枚举值对照表(请求传数字)

枚举取值(数字 = 名称)用于
Priority0=None, 1=VeryLow, 2=Low, 3=Medium, 4=High, 5=VeryHighhabits/notes/tasks 的 priority
Period0=Day, 1=Week, 2=Month, 3=Year习惯重复周期 repeatPeriod
DisplayMetric0=Repetitions, 1=Time, 2=Quantity习惯计量方式 displayMetric
DateCompare0=Before, 1=On, 2=After, 3=NotOnQuery 域时间比较 doneAtCompare/plannedAtCompare
Ratio0=ElapsedToAverage, 1=ElapsedToDesired, 2=AverageToDesiredQuery 域完成比例口径 selectedRatio
FilterDisplay0=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 错误响应形态(实测三类)

状态码形态实测样例
400ProblemDetails JSON(application/problem+json)枚举传字符串(Q2)、复数端点传错体形态(Q3/Q4)、PUT 缺 Id
401空响应体(数据接口)或纯文本(登录/刷新)见 4.5 节汇总表
404ProblemDetails JSONGET /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 节即第二轮完整实测)。

九、参考链接