Files
web-Iot/services/status/server.js

126 lines
5.0 KiB
JavaScript
Raw Normal View History

2026-08-10 13:33:06 +08:00
/**
* 机器人智慧服务系统 · 状态服务 Alpha (status-service)
* 端口: 3020
* 职责: 设备状态上报接收、最新状态缓存、历史状态查询、SSE 实时订阅
* 数据: services/status/data/status.json(本地文件替代数据库)
* 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md
*/
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3020;
const DATA_FILE = path.join(__dirname, 'data', 'status.json');
const MAX_HISTORY_PER_DEVICE = 500; // 每设备历史条数上限(超出丢弃最旧)
app.use(cors());
app.use(express.json());
// ─── 文件读写 ─────────────────────────────────────────────
function readData() {
try {
const raw = fs.readFileSync(DATA_FILE, 'utf-8');
return JSON.parse(raw);
} catch {
return { latest: {}, history: {} };
}
}
function writeData(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8');
}
// ─── SSE 订阅者 ───────────────────────────────────────────
const subscribers = new Set(); // res 集合
function broadcast(payload) {
const msg = `data: ${JSON.stringify(payload)}\n\n`;
for (const res of subscribers) {
try { res.write(msg); } catch { subscribers.delete(res); }
}
}
// ─── 路由:上报 ───────────────────────────────────────────
// POST /api/status/report 状态上报(设备 -> 平台)
// body: { deviceId, position?, battery?, mode?, speed?, sensors?, alarms?, timestamp? }
app.post('/api/status/report', (req, res) => {
const {
deviceId, position, battery, mode, speed, sensors, alarms, timestamp, meta = {},
} = req.body || {};
if (!deviceId) {
return res.status(400).json({ code: 40001, msg: 'deviceId 为必填项' });
}
const now = new Date().toISOString();
const record = {
deviceId,
position: position || null, // {x,y,z,yaw}
battery: battery ?? null, // 0~100 整数百分比
mode: mode || null, // idle/patrol/charging/working/emergency
speed: speed ?? null, // m/s
sensors: sensors || {}, // 传感器键值
alarms: alarms || [], // 告警编码列表
meta,
ts: timestamp || now, // 设备侧时间(ISO)
receivedAt: now, // 平台接收时间(ISO)
};
const data = readData();
data.latest[deviceId] = record;
const history = data.history[deviceId] || (data.history[deviceId] = []);
history.push(record);
if (history.length > MAX_HISTORY_PER_DEVICE) history.splice(0, history.length - MAX_HISTORY_PER_DEVICE);
writeData(data);
// 实时广播给订阅者
broadcast({ type: 'status', deviceId, record });
res.json({ code: 0, msg: '状态已接收', data: { deviceId, receivedAt: now } });
});
// ─── 路由:查询 ───────────────────────────────────────────
// GET /api/status/summary 全设备最新状态汇总
app.get('/api/status/summary', (req, res) => {
const data = readData();
res.json({ code: 0, data: Object.values(data.latest) });
});
// GET /api/status/:deviceId/latest 指定设备最新状态
app.get('/api/status/:deviceId/latest', (req, res) => {
const data = readData();
const record = data.latest[req.params.deviceId];
if (!record) return res.status(404).json({ code: 40401, msg: '该设备暂无状态上报' });
res.json({ code: 0, data: record });
});
// GET /api/status/history/:deviceId?from=&to=&limit= 历史状态
app.get('/api/status/history/:deviceId', (req, res) => {
const data = readData();
const history = data.history[req.params.deviceId] || [];
const { from, to, limit } = req.query;
let list = history;
if (from) list = list.filter((r) => r.ts >= from);
if (to) list = list.filter((r) => r.ts <= to);
const n = Math.min(parseInt(limit, 10) || 100, 500);
res.json({ code: 0, data: list.slice(-n) });
});
// GET /api/status/subscribe SSE 实时订阅
app.get('/api/status/subscribe', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write(`data: ${JSON.stringify({ type: 'connected', msg: '订阅成功' })}\n\n`);
subscribers.add(res);
req.on('close', () => subscribers.delete(res));
});
app.listen(PORT, () => {
console.log(`✅ 状态服务 Alpha 已启动 → http://localhost:${PORT}`);
console.log(` API 示例: POST /api/status/report {deviceId, position, battery, mode}`);
console.log(` GET /api/status/:deviceId/latest`);
console.log(` GET /api/status/subscribe (SSE)`);
});