routes_monitor_map.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import { Hono } from "hono";
  2. import debug from "debug";
  3. import type {
  4. DeviceInstance,
  5. DeviceType,
  6. DeviceTreeNode,
  7. MapViewDevice,
  8. DeviceMapStats,
  9. DeviceMapDataResponse,
  10. DeviceTreeStats
  11. } from "../client/share/monitorTypes.ts";
  12. import {
  13. DeviceStatus,
  14. DeviceTreeNodeType,
  15. DeviceTreeNodeStatus
  16. } from "../client/share/monitorTypes.ts";
  17. import {
  18. EnableStatus,
  19. DeleteStatus,
  20. } from "../client/share/types.ts";
  21. import type { Variables, WithAuth } from "./middlewares.ts";
  22. const log = {
  23. api: debug("api:monitor"),
  24. };
  25. // 定义扩展的设备实例接口,包含从数据库查询返回的额外字段
  26. interface DeviceInstanceExtended extends DeviceInstance {
  27. type_code: string;
  28. name?: string;
  29. device_status?: DeviceStatus;
  30. description?: string;
  31. longitude?: number;
  32. latitude?: number;
  33. }
  34. // 定义设备统计数据接口
  35. interface DeviceStatisticsDB {
  36. type_code: string;
  37. total: number;
  38. online: number;
  39. offline: number;
  40. error: number;
  41. }
  42. // 定义本地统计对象接口
  43. interface DeviceStats {
  44. total: number;
  45. online: number;
  46. offline: number;
  47. error: number;
  48. }
  49. // 监控数据API路由
  50. export function createMonitorMapRoutes(withAuth: WithAuth) {
  51. const monitorRoutes = new Hono<{ Variables: Variables }>();
  52. // 获取设备地图监控数据列表
  53. monitorRoutes.get("/", withAuth, async (c) => {
  54. try {
  55. const apiClient = c.get('apiClient');
  56. // 获取分页参数
  57. const page = Number(c.req.query("page")) || 1;
  58. const limit = Number(c.req.query("limit")) || 10;
  59. const offset = (page - 1) * limit;
  60. // 获取筛选参数
  61. const deviceId = c.req.query("device_id") ? Number(c.req.query("device_id")) : undefined;
  62. const deviceName = c.req.query("device_name");
  63. const protocol = c.req.query("protocol");
  64. const address = c.req.query("address");
  65. const metricType = c.req.query("metric_type");
  66. const status = c.req.query("status") ? Number(c.req.query("status")) : undefined;
  67. // 构建查询
  68. let query = apiClient.database
  69. .table("device_monitor_data")
  70. .orderBy("collect_time", "desc");
  71. // 应用筛选条件
  72. if (deviceId) {
  73. query = query.where("device_id", deviceId);
  74. }
  75. if (deviceName) {
  76. query = query.where("device_name", "like", `%${deviceName}%`);
  77. }
  78. if (protocol) {
  79. query = query.where("protocol", protocol);
  80. }
  81. if (address) {
  82. query = query.where("address", "like", `%${address}%`);
  83. }
  84. if (metricType) {
  85. query = query.where("metric_type", metricType);
  86. }
  87. if (status !== undefined) {
  88. query = query.where("status", status);
  89. }
  90. // 克隆查询以获取总数
  91. const countQuery = query.clone();
  92. // 执行分页查询
  93. const monitorData = await query.limit(limit).offset(offset);
  94. // 获取总数
  95. const count = await countQuery.count();
  96. return c.json({
  97. data: monitorData,
  98. total: Number(count),
  99. page,
  100. pageSize: limit,
  101. });
  102. } catch (error) {
  103. log.api("获取设备地图监控数据失败:", error);
  104. return c.json({ error: "获取设备地图监控数据失败" }, 500);
  105. }
  106. });
  107. // 获取设备树数据
  108. monitorRoutes.get("/devices/tree", withAuth, async (c) => {
  109. try {
  110. const apiClient = c.get('apiClient');
  111. // 获取查询参数
  112. const status = c.req.query('status');
  113. const keyword = c.req.query('keyword');
  114. // 获取所有设备分类
  115. const deviceTypes = await apiClient.database
  116. .table("device_types")
  117. .where("is_deleted", DeleteStatus.NOT_DELETED)
  118. .where("is_enabled", EnableStatus.ENABLED)
  119. .orderBy("id", "asc") as DeviceType[];
  120. // 构建设备查询
  121. let deviceQuery = apiClient.database
  122. .raw(`
  123. SELECT d.*, t.code as type_code, a.asset_name as name, a.device_status as device_status
  124. FROM device_instances d
  125. LEFT JOIN device_types t ON d.type_id = t.id
  126. LEFT JOIN zichan_info a ON d.id = a.id
  127. WHERE d.is_deleted = ? AND a.is_deleted = ?
  128. `, [DeleteStatus.NOT_DELETED, DeleteStatus.NOT_DELETED]);
  129. // 根据状态过滤
  130. if (status && status !== 'all') {
  131. deviceQuery = apiClient.database
  132. .raw(`
  133. SELECT d.*, t.code as type_code, a.asset_name as name, a.device_status as device_status
  134. FROM device_instances d
  135. LEFT JOIN device_types t ON d.type_id = t.id
  136. LEFT JOIN zichan_info a ON d.id = a.id
  137. WHERE d.is_deleted = ? AND a.is_deleted = ?
  138. AND a.device_status = ?
  139. `, [DeleteStatus.NOT_DELETED, DeleteStatus.NOT_DELETED,
  140. status === 'error' ? DeviceStatus.FAULT :
  141. status === 'normal' ? DeviceStatus.NORMAL :
  142. status === 'offline' ? DeviceStatus.OFFLINE : null]);
  143. }
  144. // 查询设备
  145. let devices = await deviceQuery as DeviceInstanceExtended[];
  146. // 根据关键词过滤
  147. if (keyword) {
  148. const lowercaseKeyword = keyword.toLowerCase();
  149. devices = devices.filter(device =>
  150. device.name?.toLowerCase().includes(lowercaseKeyword) ||
  151. device.id.toString().includes(lowercaseKeyword));
  152. }
  153. // 构建树结构
  154. const filteredTypes = deviceTypes.filter(type => {
  155. // 筛选当前分类下的设备
  156. const typeDevices = devices.filter(device => device.type_code === type.code);
  157. // 如果没有匹配的设备且有关键词或状态过滤,则跳过此分类
  158. return !(typeDevices.length === 0 && (keyword || (status && status !== 'all')));
  159. });
  160. const treeData: DeviceTreeNode[] = filteredTypes.map(type => {
  161. // 筛选当前分类下的设备
  162. const typeDevices = devices.filter(device => device.type_code === type.code);
  163. // 设备节点
  164. const deviceNodes: DeviceTreeNode[] = typeDevices.map(device => ({
  165. key: `device-${device.id}`,
  166. title: device.name || `设备${device.id}`,
  167. type: DeviceTreeNodeType.DEVICE,
  168. status: device.device_status === DeviceStatus.NORMAL ? DeviceTreeNodeStatus.NORMAL :
  169. device.device_status === DeviceStatus.FAULT ? DeviceTreeNodeStatus.ERROR :
  170. device.device_status === DeviceStatus.OFFLINE ? DeviceTreeNodeStatus.OFFLINE : DeviceTreeNodeStatus.WARNING,
  171. icon: type.image_url || null,
  172. isLeaf: true,
  173. }));
  174. return {
  175. key: type.code,
  176. title: type.name,
  177. type: DeviceTreeNodeType.CATEGORY,
  178. icon: type.image_url || null,
  179. children: deviceNodes
  180. };
  181. });
  182. return c.json({
  183. data: treeData
  184. });
  185. } catch (error) {
  186. log.api("获取设备树数据失败:", error);
  187. return c.json({ error: "获取设备树数据失败" }, 500);
  188. }
  189. });
  190. // 获取设备统计数据
  191. monitorRoutes.get("/devices/tree/statistics", withAuth, async (c) => {
  192. try {
  193. const apiClient = c.get('apiClient');
  194. // 获取所有设备分类
  195. const deviceTypes = await apiClient.database
  196. .table("device_types")
  197. .where("is_deleted", DeleteStatus.NOT_DELETED)
  198. .where("is_enabled", EnableStatus.ENABLED);
  199. // 获取设备统计
  200. const devicesStats = await apiClient.database
  201. .raw(`
  202. SELECT
  203. t.code as type_code,
  204. COUNT(*) as total,
  205. SUM(CASE WHEN a.device_status = ? THEN 1 ELSE 0 END) as online,
  206. SUM(CASE WHEN a.device_status = ? THEN 1 ELSE 0 END) as offline,
  207. SUM(CASE WHEN a.device_status = ? THEN 1 ELSE 0 END) as error
  208. FROM device_instances d
  209. LEFT JOIN device_types t ON d.type_id = t.id
  210. LEFT JOIN zichan_info a ON d.id = a.id
  211. WHERE d.is_deleted = ? AND a.is_deleted = ?
  212. GROUP BY t.code
  213. `, [DeviceStatus.NORMAL, DeviceStatus.OFFLINE, DeviceStatus.FAULT, DeleteStatus.NOT_DELETED, DeleteStatus.NOT_DELETED]) as DeviceStatisticsDB[];
  214. // 构建统计数据
  215. const statistics: DeviceTreeStats = {};
  216. deviceTypes.forEach(type => {
  217. const stats = devicesStats.find((stat: DeviceStatisticsDB) =>
  218. stat.type_code === type.code
  219. );
  220. statistics[type.code] = {
  221. total: stats ? Number(stats.total) : 0,
  222. online: stats ? Number(stats.online) : 0,
  223. offline: stats ? Number(stats.offline) : 0,
  224. error: stats ? Number(stats.error) : 0
  225. };
  226. });
  227. return c.json({
  228. data: statistics
  229. });
  230. } catch (error) {
  231. log.api("获取设备统计数据失败:", error);
  232. return c.json({ error: "获取设备统计数据失败" }, 500);
  233. }
  234. });
  235. // 获取设备地图数据
  236. monitorRoutes.get("/devices/map", withAuth, async (c) => {
  237. try {
  238. const apiClient = c.get('apiClient');
  239. // 获取筛选参数
  240. const typeCode = c.req.query("type_code");
  241. const status = c.req.query("device_status");
  242. const keyword = c.req.query("keyword");
  243. const deviceId = c.req.query("device_id");
  244. // 构建查询
  245. const query = apiClient.database
  246. .raw(`
  247. SELECT
  248. d.id, t.code as type_code, d.remark as description,
  249. a.asset_name as name, a.device_status as device_status,
  250. a.longitude, a.latitude
  251. FROM device_instances d
  252. LEFT JOIN device_types t ON d.type_id = t.id
  253. LEFT JOIN zichan_info a ON d.id = a.id
  254. WHERE d.is_deleted = ? AND a.is_deleted = ?
  255. AND a.longitude IS NOT NULL AND a.latitude IS NOT NULL
  256. `, [DeleteStatus.NOT_DELETED, DeleteStatus.NOT_DELETED]);
  257. // 将原始数据转换为设备地图数据
  258. let devices = await query as DeviceInstanceExtended[];
  259. // 获取设备类型图标
  260. const deviceTypes = await apiClient.database
  261. .table("device_types")
  262. .where("is_deleted", DeleteStatus.NOT_DELETED);
  263. // 构建图标映射
  264. const iconMap: Record<string, string> = {};
  265. deviceTypes.forEach(type => {
  266. iconMap[type.code] = type.image_url || '';
  267. });
  268. // 应用筛选
  269. if (typeCode) {
  270. devices = devices.filter(d => d.type_code === typeCode);
  271. }
  272. if (status) {
  273. const statusNum = Number(status);
  274. devices = devices.filter(d => d.device_status === statusNum);
  275. }
  276. // 设备ID精确匹配
  277. if (deviceId) {
  278. const deviceIdNum = Number(deviceId);
  279. devices = devices.filter(d => d.id === deviceIdNum);
  280. }
  281. if (keyword) {
  282. const keywordLower = keyword.toLowerCase();
  283. devices = devices.filter(d =>
  284. (d.name && d.name.toLowerCase().includes(keywordLower)) ||
  285. (d.description && d.description.toLowerCase().includes(keywordLower))
  286. );
  287. }
  288. // 将设备转换为地图标记
  289. const mapDevices = devices.map(device => ({
  290. ...device,
  291. longitude: Number(device.longitude || 0),
  292. latitude: Number(device.latitude || 0),
  293. isOnline: device.device_status === DeviceStatus.NORMAL ? '1' : '0',
  294. image_url: iconMap[device.type_code] || '',
  295. })) as MapViewDevice[];
  296. // 生成统计数据
  297. const stats: DeviceMapStats = {
  298. total: devices.length,
  299. online: devices.filter(d => d.device_status === DeviceStatus.NORMAL).length,
  300. offline: devices.filter(d => d.device_status === DeviceStatus.OFFLINE).length,
  301. error: devices.filter(d => d.device_status === DeviceStatus.FAULT).length,
  302. };
  303. return c.json({
  304. data: mapDevices,
  305. stats: stats
  306. } as DeviceMapDataResponse);
  307. } catch (error) {
  308. log.api("获取设备地图数据失败:", error);
  309. return c.json({ error: "获取设备地图数据失败" }, 500);
  310. }
  311. });
  312. return monitorRoutes;
  313. }