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 知识
HTTP POST 请求结构
Section titled “HTTP POST 请求结构”创建记录的 TimeTagger API POST 请求需要:
POST /api/v2/records HTTP/1.1Host: localhost:8820Authorization: Bearer 您的令牌Content-Type: application/json
[JSON 主体 - 记录对象数组]重要提示:请求主体必须是一个 JSON 数组,而非单个对象,即使只创建一条记录也是如此。
记录对象字段
Section titled “记录对象字段”| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
key | 字符串 | 是 | 客户端生成的唯一标识符 |
t1 | 数字 | 是 | 开始时间(Unix 时间戳,秒) |
t2 | 数字 | 是 | 结束时间(Unix 时间戳,秒) |
ds | 字符串 | 是 | 时间记录的描述 |
mt | 数字 | 是 | 修改时间(Unix 时间戳) |
st | 数字 | 否 | 服务器时间(自动设置,可为 0) |
hidden | 布尔 | 否 | 软删除标志(默认:false) |
Node-RED HTTP 请求节点设置
Section titled “Node-RED HTTP 请求节点设置”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 令牌认证 |
步骤 2:生成排序正确的记录键
Section titled “步骤 2:生成排序正确的记录键”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;步骤 3:处理 API 响应
Section titled “步骤 3:处理 API 响应”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": [] }]步骤 5:更新现有记录(PUT)
Section titled “步骤 5:更新现有记录(PUT)”对于签退流程,我们需要使用结束时间更新现有记录:
// 函数节点:"更新记录 - 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 实现:
# 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 请求正确更新现有记录
问题 1:“无效键”错误
Section titled “问题 1:“无效键”错误”症状: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);}问题 2:“t1 >= t2”错误
Section titled “问题 2:“t1 >= t2”错误”症状:API 返回时间戳验证错误
原因:开始时间晚于或等于结束时间
解决方案:
// 确保 t1 < t2if (record.t1 >= record.t2) { node.warn("修复无效的时间戳:t1 >= t2"); record.t2 = record.t1 + 60; // 设置最小 1 分钟持续时间}问题 3:HTTP 413 负载过大
Section titled “问题 3:HTTP 413 负载过大”症状:发送大数组时 HTTP 状态 413
原因:单个请求中发送了太多记录
解决方案:以 50-100 条为一批进行记录分批发送
- ✅ 建议:使用基于时间戳的键实现按时间排序
- ✅ 建议:发送请求前验证
t1 < t2 - ✅ 建议:处理 HTTP 409(冲突)重复键错误,使用新键重试
- ❌ 避免:每次 API 请求发送超过 100 条记录
- ❌ 避免:直接在函数节点代码中硬编码令牌
- POST 请求需要一个 JSON 数组的记录对象,包含
key、t1、t2、ds - 记录键由客户端生成——使用时间戳 + 随机后缀确保唯一性
- 时间戳是 Unix 纪元秒(而不是毫秒)
- PUT 请求更新现有记录(用于签退完成)
- API 响应确认创建成功,返回
ok: true和服务器时间戳