跳转到内容

HTTP POST请求实现

HTTP POST请求实现

本节介绍如何从 Node-RED 构建和发送 HTTP POST 请求到 TimeTagger API,以创建时间记录。学习完本节后,您将能够:

  • 构建一个带有 JSON 负载的有效 TimeTagger API POST 请求
  • 为新的时间记录生成唯一的记录键
  • 正确格式化时间戳以供 API 使用
  • 处理 API 响应(成功、失败、验证错误)

开始本节前,请确保您已完成:

  • 已获取 TimeTagger API 令牌(参见 05-06)
  • Node-RED HTTP 请求节点可用
  • 理解 JSON 数据格式
  • 用于函数节点的基本 JavaScript 知识

创建记录的 TimeTagger API POST 请求需要:

POST /api/v2/records HTTP/1.1
Host: localhost:8820
Authorization: Bearer 您的令牌
Content-Type: application/json
[JSON 主体 - 记录对象数组]

重要提示:请求主体必须是一个 JSON 数组,而非单个对象,即使只创建一条记录也是如此。

字段类型必需描述
key字符串客户端生成的唯一标识符
t1数字开始时间(Unix 时间戳,秒)
t2数字结束时间(Unix 时间戳,秒)
ds字符串时间记录的描述
mt数字修改时间(Unix 时间戳)
st数字服务器时间(自动设置,可为 0)
hidden布尔软删除标志(默认:false)
Node-RED 节点配置:
┌─────────────────────────────────────────┐
│ HTTP 请求节点 │
├─────────────────────────────────────────┤
│ 方法:POST │
│ URL:http://localhost:8820/api/v2/records │
│ 认证:无(使用头部) │
│ 启用 SSL/TLS:否 │
│ 返回:UTF-8 字符串 │
└─────────────────────────────────────────┘

步骤 1:创建用于记录负载的函数节点

Section titled “步骤 1:创建用于记录负载的函数节点”

第一步是创建一个生成有效记录对象的 Node-RED 函数:

// 函数节点:"创建记录负载"
// 生成一个新的 TimeTagger 时间记录
// 配置 - 在生产中使用环境变量
const authToken = "eyJhbGciOiJIUzI1NiIs..."; // 来自 TimeTagger 账户
// 为此记录生成唯一键
// TimeTagger 要求客户端生成唯一键
function generateKey() {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let key = "";
for (let i = 0; i < 24; i++) {
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
return key;
}
// 当前时间戳(Unix 秒)
const now = Math.floor(Date.now() / 1000);
// 记录描述来自 MQTT 消息负载
// msg.payload 包含 RFID 标签 UID 和用户名
const description = msg.payload.description || "RFID 签到";
// 创建记录对象
const record = {
key: generateKey(),
t1: now, // 开始时间 = 现在
t2: now + 3600, // 结束时间 = 现在 + 1 小时(占位)
ds: description,
mt: now,
st: 0 // 让服务器设置此值
};
// TimeTagger API 期望一个记录数组
const payload = [record];
// 设置 HTTP 头部
msg.headers = {
"Authorization": "Bearer " + authToken,
"Content-Type": "application/json"
};
// 设置请求主体
msg.payload = payload;
return msg;

代码说明

元素描述
generateKey()创建一个 24 字符的随机字符串作为唯一标识符
Math.floor(Date.now() / 1000)当前 Unix 时间戳(秒)
[record]数组包装(API 要求)
Authorization 头部Bearer 令牌认证

TimeTagger 通过键对记录进行排序,以实现有序排列。使用基于时间戳的键可确保按时间顺序排序:

// 函数节点:"创建带时间戳键的记录"
// 使用基于时间戳的键实现按时间排序
const authToken = global.get("timetaggerAuth").token;
const now = Math.floor(Date.now() / 1000);
// 键格式:时间戳 + 随机后缀
// 这确保记录按时间顺序排序
const timestampHex = now.toString(16); // 转换为十六进制以紧凑表示
const randomSuffix = Math.random().toString(36).substring(2, 10);
const recordKey = timestampHex + randomSuffix;
const record = {
key: recordKey,
t1: msg.payload.t1 || now,
t2: msg.payload.t2 || (now + 300), // 默认持续 5 分钟
ds: msg.payload.description || "通过 Node-RED 的时间记录",
mt: now,
st: 0
};
msg.headers = {
"Authorization": "Bearer " + authToken,
"Content-Type": "application/json"
};
msg.payload = [record];
return msg;

POST 请求后,处理响应以确认成功:

// 函数节点:"处理 POST 响应"
// 处理 TimeTagger API 响应
// 解析响应
let response;
try {
response = JSON.parse(msg.payload);
} catch (e) {
node.error("解析 TimeTagger 响应失败:" + e.toString());
msg.success = false;
msg.error = "JSON 解析错误";
return msg;
}
if (response.ok === true) {
// 成功!记录已创建
msg.success = true;
msg.recordKey = response.key;
msg.statusCode = 201;
node.warn("TimeTagger 记录已创建:" + response.key);
} else {
// API 返回错误
msg.success = false;
msg.error = response.error || "未知错误";
msg.statusCode = response.http_status || 500;
node.warn("TimeTagger API 错误:" + msg.error);
}
return msg;

步骤 4:在 Node-RED 中测试 POST 端点

Section titled “步骤 4:在 Node-RED 中测试 POST 端点”

创建一个测试流程:

[注入] → [函数:创建记录] → [HTTP 请求] → [函数:解析响应] → [调试]

测试流程 Node-RED JSON(导入此流程):

[
{
"id": "test-inject",
"type": "inject",
"name": "测试创建记录",
"props": [{"p": "payload"}],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "{\"description\":\"来自 Node-RED 的测试\",\"t1\":0,\"t2\":0}",
"payloadType": "json",
"wires": [["test-create-record"]]
},
{
"id": "test-create-record",
"type": "function",
"name": "创建记录",
"func": "const authToken = \"您的_TOKEN\";\nconst now = Math.floor(Date.now() / 1000);\n\nconst key = now.toString(16) + Math.random().toString(36).substring(2, 10);\nconst record = {\n key: key,\n t1: msg.payload.t1 || now,\n t2: msg.payload.t2 || (now + 1800),\n ds: msg.payload.description || \"测试记录\",\n mt: now,\n st: 0\n};\n\nmsg.headers = {\n \"Authorization\": \"Bearer \" + authToken,\n \"Content-Type\": \"application/json\"\n};\n\nmsg.payload = [record];\nmsg.method = \"POST\";\nmsg.url = \"http://localhost:8820/api/v2/records\";\n\nreturn msg;",
"wires": [["test-http-request"]]
},
{
"id": "test-http-request",
"type": "http request",
"name": "POST 到 TimeTagger",
"method": "POST",
"ret": "txt",
"url": "http://localhost:8820/api/v2/records",
"tls": "",
"proxy": "",
"authType": "",
"x": 450,
"y": 280,
"wires": [["test-parse-response"]]
},
{
"id": "test-parse-response",
"type": "function",
"name": "解析响应",
"func": "let response;\ntry {\n response = JSON.parse(msg.payload);\n} catch(e) {\n msg.success = false;\n msg.error = e.toString();\n return msg;\n}\n\nif (response.ok === true) {\n msg.success = true;\n msg.recordKey = msg.payload;\n node.warn(\"记录创建成功\");\n} else {\n msg.success = false;\n msg.error = response.error;\n}\n\nreturn msg;",
"wires": [["test-debug"]]
},
{
"id": "test-debug",
"type": "debug",
"name": "结果",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 650,
"y": 280,
"wires": []
}
]

对于签退流程,我们需要使用结束时间更新现有记录:

// 函数节点:"更新记录 - PUT 请求"
// 使用结束时间更新现有记录
const authToken = "您的_TOKEN";
const now = Math.floor(Date.now() / 1000);
// msg.payload 包含来自存储文件的原始记录
const existingRecord = msg.payload;
// 更新结束时间和修改时间
existingRecord.t2 = now;
existingRecord.mt = now;
msg.headers = {
"Authorization": "Bearer " + authToken,
"Content-Type": "application/json"
};
// PUT 方法用于更新
msg.method = "PUT";
msg.url = "http://localhost:8820/api/v2/records";
msg.payload = [existingRecord]; // 仍然需要数组包装
return msg;

验证 POST 实现:

Terminal window
# 1. 在 Node-RED 中触发测试流程
# 2. 检查调试选项卡中的成功响应
# 预期成功响应:
# {
# "ok": true,
# "key": "a1b2c3d4e5f6g7h8i9j0k1l2",
# "st": 1715942400,
# "from_epoch": 0,
# "to_epoch": 0
# }
# 3. 在 TimeTagger Web UI 中验证
# 打开 http://localhost:8820
# 新记录应出现在时间线中
# 4. 通过 API GET 请求验证
curl -s -X GET "http://localhost:8820/api/v2/records" \
-H "Authorization: Bearer 您的_TOKEN" | json_pp
# 预期:包含新创建的记录的数组

验证清单

  • POST 请求返回 HTTP 201(已创建)
  • 新记录出现在 TimeTagger Web UI 中
  • 描述与发送的内容匹配
  • 时间戳正确(t1 < t2)
  • 键是唯一的(无重复键错误)
  • PUT 请求正确更新现有记录

症状:API 返回 {"ok": false, "error": "Invalid key"}

原因:键包含无效字符或太短

解决方案

// 仅使用小写字母和数字
function createValidKey() {
const now = Date.now().toString(36);
const rand = Math.random().toString(36).substring(2, 12);
return (now + rand).substring(0, 24);
}

症状:API 返回时间戳验证错误

原因:开始时间晚于或等于结束时间

解决方案

// 确保 t1 < t2
if (record.t1 >= record.t2) {
node.warn("修复无效的时间戳:t1 >= t2");
record.t2 = record.t1 + 60; // 设置最小 1 分钟持续时间
}

症状:发送大数组时 HTTP 状态 413

原因:单个请求中发送了太多记录

解决方案:以 50-100 条为一批进行记录分批发送

  • 建议:使用基于时间戳的键实现按时间排序
  • 建议:发送请求前验证 t1 < t2
  • 建议:处理 HTTP 409(冲突)重复键错误,使用新键重试
  • 避免:每次 API 请求发送超过 100 条记录
  • 避免:直接在函数节点代码中硬编码令牌
  1. POST 请求需要一个 JSON 数组的记录对象,包含 keyt1t2ds
  2. 记录键由客户端生成——使用时间戳 + 随机后缀确保唯一性
  3. 时间戳是 Unix 纪元秒(而不是毫秒)
  4. PUT 请求更新现有记录(用于签退完成)
  5. API 响应确认创建成功,返回 ok: true 和服务器时间戳