label_service.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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,
  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 _trim_algorithm_row(cursor: Any, row: dict[str, Any], start: datetime, end: datetime) -> None:
  146. row_start = _parse_time(_time_string(row["start_time"]))
  147. row_end = _parse_time(_time_string(row["end_time"]))
  148. if row_end <= start or row_start >= end:
  149. return
  150. keep_before = row_start < start
  151. keep_after = row_end > end
  152. if keep_before and keep_after:
  153. cursor.execute(
  154. "UPDATE compressor_label SET end_time = %s WHERE id = %s",
  155. (_time_string(start), row["id"]),
  156. )
  157. clone = dict(row)
  158. clone["start_time"] = _time_string(end)
  159. clone["end_time"] = _time_string(row_end)
  160. clone["algorithm_start"] = row.get("algorithm_start") or row["start_time"]
  161. clone["algorithm_end"] = row.get("algorithm_end") or row["end_time"]
  162. base = str(row.get("segment_key") or row["id"])
  163. clone["segment_key"] = hashlib.md5(f"{base}|split|{_time_string(end)}".encode("utf-8")).hexdigest()
  164. clone["origin"] = "algorithm"
  165. _insert_row(cursor, clone)
  166. elif keep_before:
  167. cursor.execute(
  168. "UPDATE compressor_label SET end_time = %s WHERE id = %s",
  169. (_time_string(start), row["id"]),
  170. )
  171. elif keep_after:
  172. cursor.execute(
  173. "UPDATE compressor_label SET start_time = %s WHERE id = %s",
  174. (_time_string(end), row["id"]),
  175. )
  176. else:
  177. cursor.execute(
  178. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s",
  179. (row["id"],),
  180. )
  181. def _apply_manual(
  182. cursor: Any,
  183. payload: dict[str, Any],
  184. exclude_id: int | None = None,
  185. carry: dict[str, Any] | None = None,
  186. ) -> int:
  187. label_type = str(payload.get("label_type") or "").strip()
  188. if label_type not in LEVELS:
  189. raise ValueError("标签类型只能是 轻微 / 异常 / 严重异常")
  190. start = _snap_hour(_parse_time(payload["start_time"]))
  191. end = _snap_hour(_parse_time(payload["end_time"]))
  192. if end <= start:
  193. raise ValueError("结束时间必须大于开始时间")
  194. channel_key = str(payload.get("channel_key") or "").strip()
  195. if not channel_key:
  196. raise ValueError("channel_key 不能为空")
  197. cursor.execute(
  198. f"""
  199. SELECT {_SELECT_FIELDS}
  200. FROM compressor_label
  201. WHERE channel_key = %s
  202. AND deleted_at IS NULL
  203. AND start_time <= %s
  204. AND end_time >= %s
  205. AND (%s IS NULL OR id <> %s)
  206. """,
  207. (channel_key, _time_string(end), _time_string(start), exclude_id, exclude_id),
  208. )
  209. rows = cursor.fetchall()
  210. for row in rows:
  211. if row["origin"] == "manual" and row["label_type"] != label_type:
  212. raise ValueError("同一子图内不同类型不能重叠或相邻,请先修改或删除相邻标注")
  213. same = [row for row in rows if row["label_type"] == label_type]
  214. merged_start = min([start] + [_parse_time(_time_string(row["start_time"])) for row in same])
  215. merged_end = max([end] + [_parse_time(_time_string(row["end_time"])) for row in same])
  216. for row in rows:
  217. if row["label_type"] != label_type and row["origin"] != "manual":
  218. _trim_algorithm_row(cursor, row, merged_start, merged_end)
  219. same_ids = [row["id"] for row in same]
  220. if same_ids:
  221. placeholders = ", ".join(["%s"] * len(same_ids))
  222. cursor.execute(
  223. f"UPDATE compressor_label SET deleted_at = NOW() WHERE id IN ({placeholders})",
  224. same_ids,
  225. )
  226. carry = carry or {}
  227. source_type = "pks_manual" if payload.get("target_type") == "pks" else "wave_manual"
  228. new_row = {
  229. "source_type": source_type,
  230. "origin": "manual",
  231. "target_type": payload.get("target_type") or ("pks" if payload.get("metric_key") else "wave_file"),
  232. "target_id": payload.get("target_id"),
  233. "target_key": channel_key,
  234. "channel_key": channel_key,
  235. "segment_key": None,
  236. "unit": str(payload.get("unit") or carry.get("unit") or ""),
  237. "device_code": payload.get("device_code") or carry.get("device_code"),
  238. "device_part": payload.get("device_part") or carry.get("device_part"),
  239. "device_point": payload.get("device_point") or carry.get("device_point"),
  240. "metric_key": payload.get("metric_key") or carry.get("metric_key"),
  241. "metric_label": payload.get("metric_label") or carry.get("metric_label"),
  242. "start_time": _time_string(merged_start),
  243. "end_time": _time_string(merged_end),
  244. "algorithm_start": carry.get("algorithm_start"),
  245. "algorithm_end": carry.get("algorithm_end"),
  246. "label_type": label_type,
  247. "label_subtype": payload.get("label_subtype") or carry.get("label_subtype"),
  248. "severity": SEVERITY_BY_LEVEL.get(label_type),
  249. "confirm_status": "人工确认",
  250. "usable_for_train": 1,
  251. "source_file": carry.get("source_file"),
  252. "source_row": carry.get("source_row"),
  253. "algorithm_version": carry.get("algorithm_version"),
  254. "features_json": carry.get("features_json"),
  255. "note": payload.get("note"),
  256. }
  257. return _insert_row(cursor, new_row)
  258. def create_label(payload: dict[str, Any]) -> dict[str, Any]:
  259. with get_connection() as connection:
  260. with connection.cursor() as cursor:
  261. connection.begin()
  262. try:
  263. label_id = _apply_manual(cursor, payload)
  264. result = _fetch_label(cursor, label_id)
  265. connection.commit()
  266. return result
  267. except Exception:
  268. connection.rollback()
  269. raise
  270. def update_label(label_id: int, payload: dict[str, Any]) -> dict[str, Any]:
  271. if label_id <= 0:
  272. raise ValueError("标注 id 必须为正整数")
  273. with get_connection() as connection:
  274. with connection.cursor() as cursor:
  275. connection.begin()
  276. try:
  277. cursor.execute(
  278. f"SELECT {_SELECT_FIELDS} FROM compressor_label WHERE id = %s AND deleted_at IS NULL",
  279. (label_id,),
  280. )
  281. old = cursor.fetchone()
  282. if not old:
  283. raise ValueError("标注不存在")
  284. new_id = _apply_manual(cursor, payload, exclude_id=label_id, carry=old)
  285. result = _fetch_label(cursor, new_id)
  286. if old["origin"] == "manual":
  287. cursor.execute(
  288. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s",
  289. (label_id,),
  290. )
  291. else:
  292. _trim_algorithm_row(
  293. cursor,
  294. old,
  295. _parse_time(result["startTime"]),
  296. _parse_time(result["endTime"]),
  297. )
  298. connection.commit()
  299. return result
  300. except Exception:
  301. connection.rollback()
  302. raise
  303. def delete_label(label_id: int) -> dict[str, Any]:
  304. if label_id <= 0:
  305. raise ValueError("标注 id 必须为正整数")
  306. with get_connection() as connection:
  307. with connection.cursor() as cursor:
  308. cursor.execute(
  309. "UPDATE compressor_label SET deleted_at = NOW() WHERE id = %s AND deleted_at IS NULL",
  310. (label_id,),
  311. )
  312. if cursor.rowcount == 0:
  313. raise ValueError("标注不存在")
  314. return {"deleted": label_id}