label_service.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. from __future__ import annotations
  2. import hashlib
  3. import json
  4. from datetime import datetime
  5. from typing import Any
  6. from ..db import get_connection
  7. LEVELS = ("轻微", "异常", "严重异常")
  8. SEVERITY_BY_LEVEL = {"轻微": 1, "异常": 2, "严重异常": 3}
  9. _SELECT_FIELDS = """
  10. id, source_type, origin, target_type, target_id, target_key, channel_key, segment_key,
  11. unit, device_code, device_part, device_point, metric_key, metric_label,
  12. start_time, end_time, algorithm_start, algorithm_end, label_type, label_subtype,
  13. severity, confirm_status, usable_for_train, source_file, source_row,
  14. algorithm_version, features_json, note
  15. """
  16. def _time_string(value: Any) -> str:
  17. if isinstance(value, datetime):
  18. return value.strftime("%Y-%m-%d %H:%M:%S")
  19. return str(value or "")
  20. def _parse_time(value: str) -> datetime:
  21. text = str(value or "").strip().replace("T", " ")
  22. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
  23. try:
  24. return datetime.strptime(text[:19] if fmt.endswith("%S") else text[:16], fmt)
  25. except ValueError:
  26. continue
  27. raise ValueError("时间格式不正确,应为 YYYY-MM-DD HH:MM:SS")
  28. def _snap_hour(value: datetime) -> datetime:
  29. return value.replace(minute=0, second=0, microsecond=0)
  30. def _row_key(row: dict[str, Any]) -> str:
  31. if row.get("target_type") == "pks":
  32. unit = str(row.get("unit") or "")
  33. metric = str(row.get("metric_key") or "")
  34. suffix = metric.split("_")[-1] if "_" in metric else metric
  35. return f"YSJ{unit}_{suffix}" if unit and suffix else metric
  36. return str(row.get("device_point") or "")
  37. def _row_dict(row: dict[str, Any]) -> dict[str, Any]:
  38. features: Any = None
  39. raw = row.get("features_json")
  40. if raw:
  41. try:
  42. features = json.loads(raw)
  43. except (TypeError, ValueError):
  44. features = raw
  45. return {
  46. "id": int(row["id"]),
  47. "sourceType": row.get("source_type") or "",
  48. "origin": row.get("origin") or "",
  49. "targetType": row.get("target_type") or "",
  50. "targetId": int(row["target_id"]) if row.get("target_id") is not None else None,
  51. "targetKey": row.get("target_key") or "",
  52. "channelKey": row.get("channel_key") or "",
  53. "unit": row.get("unit") or "",
  54. "deviceCode": row.get("device_code") or "",
  55. "devicePart": row.get("device_part") or "",
  56. "devicePoint": row.get("device_point") or "",
  57. "metricKey": row.get("metric_key") or "",
  58. "metricLabel": row.get("metric_label") or "",
  59. "startTime": _time_string(row.get("start_time")),
  60. "endTime": _time_string(row.get("end_time")),
  61. "labelType": row.get("label_type") or "",
  62. "labelSubtype": row.get("label_subtype"),
  63. "severity": int(row["severity"]) if row.get("severity") is not None else None,
  64. "confirmStatus": row.get("confirm_status") or "",
  65. "usableForTrain": bool(row.get("usable_for_train")),
  66. "sourceRow": row.get("source_row") or "",
  67. "features": features,
  68. "note": row.get("note"),
  69. "rowKey": _row_key(row),
  70. }
  71. def _fetch_label(cursor: Any, label_id: int) -> dict[str, Any]:
  72. cursor.execute(f"SELECT {_SELECT_FIELDS} FROM compressor_label WHERE id = %s", (label_id,))
  73. row = cursor.fetchone()
  74. if not row:
  75. raise ValueError("标注不存在")
  76. return _row_dict(row)
  77. def query_labels(
  78. target_type: str | None = None,
  79. unit: str | None = None,
  80. device_code: str | None = None,
  81. metric_keys: list[str] | None = None,
  82. channel_keys: list[str] | None = None,
  83. wave_file_ids: list[int] | None = None,
  84. start: str | None = None,
  85. end: str | None = None,
  86. ) -> dict[str, Any]:
  87. clauses = ["deleted_at IS NULL"]
  88. params: list[Any] = []
  89. if target_type:
  90. clauses.append("target_type = %s")
  91. params.append(target_type)
  92. if unit:
  93. clauses.append("unit = %s")
  94. params.append(unit)
  95. if device_code:
  96. clauses.append("device_code = %s")
  97. params.append(device_code)
  98. if metric_keys:
  99. placeholders = ", ".join(["%s"] * len(metric_keys))
  100. clauses.append(f"metric_key IN ({placeholders})")
  101. params.extend(metric_keys)
  102. if channel_keys:
  103. placeholders = ", ".join(["%s"] * len(channel_keys))
  104. clauses.append(f"channel_key IN ({placeholders})")
  105. params.extend(channel_keys)
  106. if wave_file_ids:
  107. placeholders = ", ".join(["%s"] * len(wave_file_ids))
  108. clauses.append(f"target_id IN ({placeholders})")
  109. params.extend(wave_file_ids)
  110. if start:
  111. clauses.append("end_time > %s")
  112. params.append(start)
  113. if end:
  114. clauses.append("start_time < %s")
  115. params.append(end)
  116. sql = f"""
  117. SELECT {_SELECT_FIELDS}
  118. FROM compressor_label
  119. WHERE {' AND '.join(clauses)}
  120. ORDER BY channel_key ASC, start_time ASC, id ASC
  121. """
  122. with get_connection() as connection:
  123. with connection.cursor() as cursor:
  124. cursor.execute(sql, params)
  125. rows = cursor.fetchall()
  126. return {
  127. "source": "database",
  128. "labels": [_row_dict(row) for row in rows],
  129. "notice": None,
  130. }
  131. def _insert_row(cursor: Any, row: dict[str, Any]) -> int:
  132. fields = (
  133. "source_type", "origin", "target_type", "target_id", "target_key", "channel_key",
  134. "segment_key", "unit", "device_code", "device_part", "device_point", "metric_key",
  135. "metric_label", "start_time", "end_time", "algorithm_start", "algorithm_end",
  136. "label_type", "label_subtype", "severity", "confirm_status", "usable_for_train",
  137. "source_file", "source_row", "algorithm_version", "features_json", "note",
  138. )
  139. placeholders = ", ".join(["%s"] * len(fields))
  140. cursor.execute(
  141. f"INSERT INTO compressor_label ({', '.join(fields)}) VALUES ({placeholders})",
  142. tuple(row.get(field) for field in fields),
  143. )
  144. return int(cursor.lastrowid)
  145. def _clone_row_with_range(cursor: Any, row: dict[str, Any], start: datetime, end: datetime) -> int:
  146. clone = dict(row)
  147. clone["start_time"] = _time_string(start)
  148. clone["end_time"] = _time_string(end)
  149. if row.get("origin") == "algorithm":
  150. base = str(row.get("segment_key") or row.get("id"))
  151. clone["segment_key"] = hashlib.md5(
  152. f"{base}|range|{_time_string(start)}|{_time_string(end)}".encode("utf-8")
  153. ).hexdigest()
  154. else:
  155. clone["segment_key"] = None
  156. return _insert_row(cursor, clone)
  157. def _trim_algorithm_row(cursor: Any, row: dict[str, Any], start: datetime, end: datetime) -> None:
  158. row_start = _parse_time(_time_string(row["start_time"]))
  159. row_end = _parse_time(_time_string(row["end_time"]))
  160. if row_end <= start or row_start >= end:
  161. return
  162. keep_before = row_start < start
  163. keep_after = row_end > end
  164. if keep_before and keep_after:
  165. cursor.execute(
  166. "UPDATE compressor_label SET end_time = %s WHERE id = %s",
  167. (_time_string(start), row["id"]),
  168. )
  169. clone = dict(row)
  170. clone["start_time"] = _time_string(end)
  171. clone["end_time"] = _time_string(row_end)
  172. clone["algorithm_start"] = row.get("algorithm_start") or row["start_time"]
  173. clone["algorithm_end"] = row.get("algorithm_end") or row["end_time"]
  174. base = str(row.get("segment_key") or row["id"])
  175. clone["segment_key"] = hashlib.md5(f"{base}|split|{_time_string(end)}".encode("utf-8")).hexdigest()
  176. clone["origin"] = "algorithm"
  177. _insert_row(cursor, clone)
  178. elif keep_before:
  179. cursor.execute(
  180. "UPDATE compressor_label SET end_time = %s WHERE id = %s",
  181. (_time_string(start), row["id"]),
  182. )
  183. elif keep_after:
  184. cursor.execute(
  185. "UPDATE compressor_label SET start_time = %s WHERE id = %s",
  186. (_time_string(end), row["id"]),
  187. )
  188. else:
  189. cursor.execute(
  190. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s",
  191. (row["id"],),
  192. )
  193. def _apply_manual(
  194. cursor: Any,
  195. payload: dict[str, Any],
  196. exclude_id: int | None = None,
  197. carry: dict[str, Any] | None = None,
  198. ) -> int:
  199. label_type = str(payload.get("label_type") or "").strip()
  200. if label_type not in LEVELS:
  201. raise ValueError("标签类型只能是 轻微 / 异常 / 严重异常")
  202. start = _snap_hour(_parse_time(payload["start_time"]))
  203. end = _snap_hour(_parse_time(payload["end_time"]))
  204. if end <= start:
  205. raise ValueError("结束时间必须大于开始时间")
  206. channel_key = str(payload.get("channel_key") or "").strip()
  207. if not channel_key:
  208. raise ValueError("channel_key 不能为空")
  209. cursor.execute(
  210. f"""
  211. SELECT {_SELECT_FIELDS}
  212. FROM compressor_label
  213. WHERE channel_key = %s
  214. AND deleted_at IS NULL
  215. AND start_time <= %s
  216. AND end_time >= %s
  217. AND (%s IS NULL OR id <> %s)
  218. """,
  219. (channel_key, _time_string(end), _time_string(start), exclude_id, exclude_id),
  220. )
  221. rows = cursor.fetchall()
  222. for row in rows:
  223. if row["origin"] == "manual" and row["label_type"] != label_type:
  224. raise ValueError("同一子图内不同类型不能重叠或相邻,请先修改或删除相邻标注")
  225. same = [row for row in rows if row["label_type"] == label_type]
  226. merged_start = min([start] + [_parse_time(_time_string(row["start_time"])) for row in same])
  227. merged_end = max([end] + [_parse_time(_time_string(row["end_time"])) for row in same])
  228. for row in rows:
  229. if row["label_type"] != label_type and row["origin"] != "manual":
  230. _trim_algorithm_row(cursor, row, merged_start, merged_end)
  231. same_ids = [row["id"] for row in same]
  232. if same_ids:
  233. placeholders = ", ".join(["%s"] * len(same_ids))
  234. cursor.execute(
  235. f"UPDATE compressor_label SET deleted_at = NOW() WHERE id IN ({placeholders})",
  236. same_ids,
  237. )
  238. carry = carry or {}
  239. source_type = "pks_manual" if payload.get("target_type") == "pks" else "wave_manual"
  240. new_row = {
  241. "source_type": source_type,
  242. "origin": "manual",
  243. "target_type": payload.get("target_type") or ("pks" if payload.get("metric_key") else "wave_file"),
  244. "target_id": payload.get("target_id"),
  245. "target_key": channel_key,
  246. "channel_key": channel_key,
  247. "segment_key": None,
  248. "unit": str(payload.get("unit") or carry.get("unit") or ""),
  249. "device_code": payload.get("device_code") or carry.get("device_code"),
  250. "device_part": payload.get("device_part") or carry.get("device_part"),
  251. "device_point": payload.get("device_point") or carry.get("device_point"),
  252. "metric_key": payload.get("metric_key") or carry.get("metric_key"),
  253. "metric_label": payload.get("metric_label") or carry.get("metric_label"),
  254. "start_time": _time_string(merged_start),
  255. "end_time": _time_string(merged_end),
  256. "algorithm_start": carry.get("algorithm_start"),
  257. "algorithm_end": carry.get("algorithm_end"),
  258. "label_type": label_type,
  259. "label_subtype": payload.get("label_subtype") or carry.get("label_subtype"),
  260. "severity": SEVERITY_BY_LEVEL.get(label_type),
  261. "confirm_status": "人工确认",
  262. "usable_for_train": 1,
  263. "source_file": carry.get("source_file"),
  264. "source_row": carry.get("source_row"),
  265. "algorithm_version": carry.get("algorithm_version"),
  266. "features_json": carry.get("features_json"),
  267. "note": payload.get("note"),
  268. }
  269. return _insert_row(cursor, new_row)
  270. def create_label(payload: dict[str, Any]) -> dict[str, Any]:
  271. with get_connection() as connection:
  272. with connection.cursor() as cursor:
  273. connection.begin()
  274. try:
  275. label_id = _apply_manual(cursor, payload)
  276. result = _fetch_label(cursor, label_id)
  277. connection.commit()
  278. return result
  279. except Exception:
  280. connection.rollback()
  281. raise
  282. def update_label(label_id: int, payload: dict[str, Any]) -> dict[str, Any]:
  283. if label_id <= 0:
  284. raise ValueError("标注 id 必须为正整数")
  285. with get_connection() as connection:
  286. with connection.cursor() as cursor:
  287. connection.begin()
  288. try:
  289. cursor.execute(
  290. f"SELECT {_SELECT_FIELDS} FROM compressor_label WHERE id = %s AND deleted_at IS NULL",
  291. (label_id,),
  292. )
  293. old = cursor.fetchone()
  294. if not old:
  295. raise ValueError("标注不存在")
  296. new_id = _apply_manual(cursor, payload, exclude_id=label_id, carry=old)
  297. result = _fetch_label(cursor, new_id)
  298. if old["origin"] == "manual":
  299. cursor.execute(
  300. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s",
  301. (label_id,),
  302. )
  303. else:
  304. _trim_algorithm_row(
  305. cursor,
  306. old,
  307. _parse_time(result["startTime"]),
  308. _parse_time(result["endTime"]),
  309. )
  310. connection.commit()
  311. return result
  312. except Exception:
  313. connection.rollback()
  314. raise
  315. def delete_label(label_id: int) -> dict[str, Any]:
  316. if label_id <= 0:
  317. raise ValueError("标注 id 必须为正整数")
  318. with get_connection() as connection:
  319. with connection.cursor() as cursor:
  320. cursor.execute(
  321. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s AND deleted_at IS NULL",
  322. (label_id,),
  323. )
  324. if cursor.rowcount == 0:
  325. raise ValueError("标注不存在")
  326. return {"deleted": label_id}
  327. def delete_label_range(label_id: int, start: str, end: str) -> dict[str, Any]:
  328. if label_id <= 0:
  329. raise ValueError("标注 id 必须为正整数")
  330. cut_start = _snap_hour(_parse_time(start))
  331. cut_end = _snap_hour(_parse_time(end))
  332. if cut_end <= cut_start:
  333. raise ValueError("结束时间必须大于开始时间")
  334. with get_connection() as connection:
  335. with connection.cursor() as cursor:
  336. connection.begin()
  337. try:
  338. cursor.execute(
  339. f"SELECT {_SELECT_FIELDS} FROM compressor_label WHERE id = %s AND deleted_at IS NULL",
  340. (label_id,),
  341. )
  342. row = cursor.fetchone()
  343. if not row:
  344. raise ValueError("标注不存在")
  345. row_start = _parse_time(_time_string(row["start_time"]))
  346. row_end = _parse_time(_time_string(row["end_time"]))
  347. lo = max(row_start, cut_start)
  348. hi = min(row_end, cut_end)
  349. if hi <= lo:
  350. raise ValueError("删除区间不在该标注内")
  351. cursor.execute(
  352. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s",
  353. (label_id,),
  354. )
  355. if lo <= row_start and hi >= row_end:
  356. connection.commit()
  357. return {
  358. "deleted": label_id,
  359. "deletedRange": [_time_string(lo), _time_string(hi)],
  360. "beforeId": None,
  361. "afterId": None,
  362. "whole": True,
  363. }
  364. before_id = None
  365. after_id = None
  366. if row_start < lo:
  367. before_id = _clone_row_with_range(cursor, row, row_start, lo)
  368. if hi < row_end:
  369. after_id = _clone_row_with_range(cursor, row, hi, row_end)
  370. connection.commit()
  371. return {
  372. "deleted": label_id,
  373. "deletedRange": [_time_string(lo), _time_string(hi)],
  374. "beforeId": before_id,
  375. "afterId": after_id,
  376. "whole": False,
  377. }
  378. except Exception:
  379. connection.rollback()
  380. raise