routes_big.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. import { Hono } from "hono";
  2. import debug from "debug";
  3. import dayjs from "dayjs";
  4. import utc from "dayjs/plugin/utc";
  5. import type {
  6. RackInfo,
  7. CategoryChartData,
  8. OnlineRateChartData,
  9. StateChartData,
  10. AlarmChartData,
  11. DeviceWithAssetInfo,
  12. AlarmDeviceData,
  13. } from "../client/share/monitorTypes.ts";
  14. import {
  15. DeviceStatus,
  16. AssetTransferType,
  17. MetricType,
  18. NetworkStatus,
  19. } from "../client/share/monitorTypes.ts";
  20. import type { Variables, WithAuth } from "./middlewares.ts";
  21. import { DeleteStatus } from "../client/share/types.ts";
  22. dayjs.extend(utc);
  23. const log = {
  24. api: debug("d8d:chart:api"),
  25. app: debug("d8d:chart:app"),
  26. };
  27. // 创建大屏路由
  28. export function createBigRoutes(withAuth: WithAuth) {
  29. const bigRoutes = new Hono<{ Variables: Variables }>();
  30. // 资产分类图表数据
  31. bigRoutes.get("/zichan_category_chart", async (c) => {
  32. try {
  33. const apiClient = c.get('apiClient');
  34. // 直接使用查询参数而不是filters层
  35. const isDeleted = c.req.query("is_deleted") === "1" ? 1 : 0;
  36. // 执行SQL查询统计各分类设备数量
  37. const result = await apiClient.database.raw(
  38. `
  39. SELECT
  40. CASE
  41. WHEN zc.name IS NULL THEN '未分类'
  42. ELSE zc.name
  43. END as 设备分类,
  44. COUNT(zi.id) as 设备数
  45. FROM
  46. zichan_info zi
  47. LEFT JOIN
  48. zichan_category zc ON zi.device_category = zc.id
  49. WHERE
  50. zi.is_deleted = ?
  51. GROUP BY
  52. zi.device_category
  53. ORDER BY
  54. 设备数 DESC
  55. `,
  56. [isDeleted]
  57. );
  58. return c.json(result as CategoryChartData[]);
  59. } catch (error) {
  60. console.error("获取设备分类统计失败:", error);
  61. return c.json([]);
  62. }
  63. });
  64. // 设备在线率变化数据
  65. bigRoutes.get("/zichan_online_rate_chart", async (c) => {
  66. try {
  67. const apiClient = c.get('apiClient');
  68. const query = c.req.query();
  69. // 直接使用扁平化查询参数
  70. const startTime =
  71. query.created_at_gte ||
  72. dayjs().subtract(7, "day").format("YYYY-MM-DD HH:mm:ss");
  73. const endTime =
  74. query.created_at_lte || dayjs().format("YYYY-MM-DD HH:mm:ss");
  75. const dimension = query.dimension || "day";
  76. // 根据维度确定日期格式
  77. let dateFormat = "%Y-%m-%d";
  78. if (dimension === "hour") {
  79. dateFormat = "%Y-%m-%d %H:00";
  80. } else if (dimension === "month") {
  81. dateFormat = "%Y-%m";
  82. }
  83. // 按日期聚合查询设备在线数量
  84. const onlineData = await apiClient.database.raw(
  85. `
  86. SELECT
  87. DATE_FORMAT(created_at, '${dateFormat}') AS time_interval,
  88. COUNT(CASE WHEN network_status = ${NetworkStatus.CONNECTED} THEN 1 END) AS online_devices,
  89. COUNT(id) AS total_devices
  90. FROM
  91. zichan_info
  92. WHERE
  93. is_deleted = ${DeleteStatus.NOT_DELETED}
  94. AND created_at BETWEEN ? AND ?
  95. GROUP BY
  96. DATE_FORMAT(created_at, '${dateFormat}')
  97. ORDER BY
  98. time_interval ASC
  99. `,
  100. [startTime, endTime]
  101. );
  102. // 返回明确类型的响应数据
  103. return c.json(onlineData as OnlineRateChartData[]);
  104. } catch (error) {
  105. console.error("获取在线率变化数据失败:", error);
  106. return c.json([]);
  107. }
  108. });
  109. // 资产状态分布数据
  110. bigRoutes.get("/zichan_state_chart", async (c) => {
  111. try {
  112. const apiClient = c.get('apiClient');
  113. // 直接使用查询参数
  114. const isDeleted = c.req.query("is_deleted") === "1" ? 1 : 0;
  115. // 执行SQL查询统计各状态设备数量
  116. const result = await apiClient.database.raw(
  117. `
  118. SELECT
  119. CASE
  120. WHEN ztl.asset_transfer = ${AssetTransferType.STOCK} THEN '设备在库'
  121. WHEN ztl.asset_transfer = ${AssetTransferType.BORROW} THEN '设备借用'
  122. WHEN ztl.asset_transfer = ${AssetTransferType.RETURN} THEN '设备归还'
  123. WHEN ztl.asset_transfer = ${AssetTransferType.LOST} THEN '设备遗失'
  124. WHEN ztl.asset_transfer = ${AssetTransferType.MAINTAIN} THEN '设备维护保养'
  125. ELSE '其他'
  126. END as 资产流转,
  127. COUNT(ztl.id) as 设备数
  128. FROM
  129. zichan_trans_log ztl
  130. LEFT JOIN
  131. zichan_info zi ON ztl.asset_id = zi.id
  132. WHERE
  133. ztl.is_deleted = ? AND zi.is_deleted = ?
  134. GROUP BY
  135. ztl.asset_transfer
  136. ORDER BY
  137. 设备数 DESC
  138. `,
  139. [isDeleted, isDeleted]
  140. );
  141. return c.json(result as StateChartData[]);
  142. } catch (error) {
  143. console.error("获取资产状态分布数据失败:", error);
  144. return c.json([]);
  145. }
  146. });
  147. // 告警数据趋势
  148. bigRoutes.get("/zichan_alarm_chart", async (c) => {
  149. try {
  150. const apiClient = c.get('apiClient');
  151. const query = c.req.query();
  152. // 直接使用扁平化查询参数
  153. const startTime =
  154. query.created_at_gte ||
  155. dayjs()
  156. .utc()
  157. .subtract(1, "day")
  158. .startOf("day")
  159. .format("YYYY-MM-DD HH:mm:ss");
  160. const endTime =
  161. query.created_at_lte ||
  162. dayjs().utc().endOf("day").format("YYYY-MM-DD HH:mm:ss");
  163. const dimension = query.dimension || "hour";
  164. log.app("startTime", startTime);
  165. log.app("endTime", endTime);
  166. // 根据维度确定日期格式
  167. let dateFormat = "%H:00";
  168. let sortFormat = "%Y-%m-%d %H:00";
  169. if (dimension === "day") {
  170. dateFormat = "%Y-%m-%d";
  171. sortFormat = "%Y-%m-%d";
  172. } else if (dimension === "month") {
  173. dateFormat = "%Y-%m";
  174. sortFormat = "%Y-%m";
  175. }
  176. // 按日期聚合查询告警数量
  177. const alarmData = (await apiClient.database.raw(
  178. `
  179. SELECT
  180. DATE_FORMAT(CONVERT_TZ(created_at, '+00:00', '+08:00'), '${dateFormat}') AS time_interval,
  181. COUNT(id) AS total_devices,
  182. DATE_FORMAT(CONVERT_TZ(created_at, '+00:00', '+08:00'), '${sortFormat}') AS sort_time
  183. FROM
  184. device_alerts
  185. WHERE
  186. is_deleted = ${DeleteStatus.NOT_DELETED}
  187. AND created_at BETWEEN ? AND ?
  188. GROUP BY
  189. DATE_FORMAT(CONVERT_TZ(created_at, '+00:00', '+08:00'), '${dateFormat}'),
  190. DATE_FORMAT(CONVERT_TZ(created_at, '+00:00', '+08:00'), '${sortFormat}')
  191. ORDER BY
  192. sort_time ASC
  193. `,
  194. [startTime, endTime]
  195. )) as AlarmChartData[];
  196. // 移除排序字段,只返回需要的数据
  197. const formattedData = alarmData.map((item) => ({
  198. time_interval: item.time_interval,
  199. total_devices: item.total_devices,
  200. }));
  201. return c.json(formattedData);
  202. } catch (error) {
  203. console.error("获取告警趋势数据失败:", error);
  204. return c.json([]);
  205. }
  206. });
  207. // 设备实例数据
  208. bigRoutes.get("/device-instances", async (c) => {
  209. try {
  210. const apiClient = c.get('apiClient');
  211. const query = c.req.query();
  212. // 直接使用查询参数
  213. const isDeleted = query.is_deleted === "1" ? 1 : 0;
  214. // 查询设备实例数据,关联资产信息和最新监控数据
  215. const deviceData = await apiClient.database.raw(
  216. `
  217. SELECT
  218. di.id,
  219. di.address as ip_address,
  220. zi.asset_name,
  221. zi.device_category,
  222. zi.cpu,
  223. zi.memory,
  224. zi.disk,
  225. di.is_deleted
  226. FROM
  227. device_instances di
  228. LEFT JOIN
  229. zichan_info zi ON di.id = zi.id
  230. WHERE
  231. di.is_deleted = ?
  232. `,
  233. [isDeleted]
  234. );
  235. const getMetricValue = async (deviceId: number, metricType: MetricType) => {
  236. const result = await apiClient.database
  237. .table("device_monitor_data")
  238. .where("device_id", deviceId)
  239. .where("metric_type", metricType)
  240. .where("is_deleted", DeleteStatus.NOT_DELETED)
  241. .first();
  242. return result?.status;
  243. };
  244. const promises = deviceData.map(async (item: DeviceWithAssetInfo) => {
  245. item.device_status =
  246. (await getMetricValue(item.id, MetricType.CONNECTION_STATUS)) ===
  247. DeviceStatus.NORMAL
  248. ? 1
  249. : 0;
  250. return item;
  251. });
  252. const newDeviceData = await Promise.all(promises);
  253. // 返回明确类型的响应数据
  254. return c.json(newDeviceData as DeviceWithAssetInfo[]);
  255. } catch (error) {
  256. console.error("获取设备实例数据失败:", error);
  257. return c.json([]);
  258. }
  259. });
  260. // 告警设备TOP数据
  261. bigRoutes.get("/zichan_alarm_device", async (c) => {
  262. try {
  263. const apiClient = c.get('apiClient');
  264. // 直接使用查询参数
  265. const limit = Number(c.req.query("limit")) || 5;
  266. // 查询告警次数最多的设备TOP10
  267. const alarmDevices = await apiClient.database.raw(
  268. `
  269. SELECT
  270. zi.asset_name AS deviceName,
  271. COUNT(da.id) AS alarmCount,
  272. ROW_NUMBER() OVER (ORDER BY COUNT(da.id) DESC) AS \`rank\`
  273. FROM
  274. device_alerts da
  275. LEFT JOIN
  276. zichan_info zi ON da.device_id = zi.id
  277. WHERE
  278. da.is_deleted = ${DeleteStatus.NOT_DELETED}
  279. AND zi.is_deleted = ${DeleteStatus.NOT_DELETED}
  280. GROUP BY
  281. da.device_id, zi.asset_name
  282. ORDER BY
  283. alarmCount DESC
  284. LIMIT ?
  285. `,
  286. [limit]
  287. );
  288. // 返回明确类型的响应数据
  289. return c.json(alarmDevices as AlarmDeviceData[]);
  290. } catch (error) {
  291. console.error("获取告警设备TOP数据失败:", error);
  292. return c.json([]);
  293. }
  294. });
  295. // 机柜数据
  296. bigRoutes.get("/rack", async (c) => {
  297. try {
  298. const apiClient = c.get('apiClient');
  299. const query = c.req.query();
  300. // 直接使用查询参数
  301. const isDeleted = query.is_deleted === "1" ? 1 : 0;
  302. // 查询机柜数据
  303. const rackData = (await apiClient.database
  304. .table("rack_info")
  305. .where("is_deleted", isDeleted)
  306. .select("*")) as RackInfo[];
  307. return c.json(rackData);
  308. } catch (error) {
  309. console.error("获取机柜数据失败:", error);
  310. return c.json([]);
  311. }
  312. });
  313. // 机柜服务器数据
  314. bigRoutes.get("/rack-server", async (c) => {
  315. try {
  316. const apiClient = c.get('apiClient');
  317. const query = c.req.query();
  318. // 直接使用查询参数
  319. const rackId = query.rack_id;
  320. const isDeleted = query.is_deleted === "1" ? 1 : 0;
  321. // 查询条件构建
  322. const queryBuilder = apiClient.database
  323. .table("rack_server as rs")
  324. .leftJoin("zichan_info as zi", "rs.asset_id", "zi.id")
  325. .leftJoin("device_instances as di", "rs.asset_id", "di.id")
  326. .leftJoin("rack_info as ri", "rs.rack_id", "ri.id")
  327. .where("rs.is_deleted", isDeleted)
  328. .select(
  329. "rs.*",
  330. "zi.asset_name",
  331. "zi.device_category",
  332. "zi.device_status",
  333. "zi.network_status",
  334. "zi.packet_loss",
  335. "zi.cpu",
  336. "zi.memory",
  337. "zi.disk",
  338. "ri.rack_name",
  339. "ri.rack_code",
  340. "di.address as ip_address"
  341. );
  342. // 添加机柜ID过滤条件
  343. if (rackId) {
  344. queryBuilder.where("rs.rack_id", rackId);
  345. }
  346. const rackServerData = await queryBuilder;
  347. return c.json(rackServerData);
  348. } catch (error) {
  349. console.error("获取机柜服务器数据失败:", error);
  350. return c.json([]);
  351. }
  352. });
  353. // 服务器类型贴图
  354. bigRoutes.get("/rack/server/type/image", async (c) => {
  355. try {
  356. const apiClient = c.get('apiClient');
  357. const query = c.req.query();
  358. // 直接使用查询参数
  359. const isEnabled = query.is_enabled === "1" ? 1 : 0;
  360. const isDeleted = query.is_deleted === "1" ? 1 : 0;
  361. // 分页参数
  362. const page = parseInt(query.page || "1");
  363. const pageSize = parseInt(query.pageSize || "100");
  364. // 查询条件构建
  365. const queryBuilder = apiClient.database
  366. .table("rack_server_type")
  367. .where("is_deleted", isDeleted)
  368. .select("*")
  369. .orderBy("id", "asc");
  370. // 添加过滤条件
  371. if (isEnabled !== undefined) {
  372. queryBuilder.where("is_enabled", isEnabled);
  373. }
  374. // 应用分页
  375. const iconData = await queryBuilder
  376. .limit(pageSize)
  377. .offset((page - 1) * pageSize);
  378. return c.json(iconData);
  379. } catch (error) {
  380. console.error("获取服务器类型贴图失败:", error);
  381. return c.json([]);
  382. }
  383. });
  384. return bigRoutes;
  385. }