label_import_service.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. from __future__ import annotations
  2. import copy
  3. import csv
  4. import hashlib
  5. import json
  6. import re
  7. import threading
  8. import time
  9. from datetime import datetime, timedelta
  10. from pathlib import Path
  11. from typing import Any, Callable
  12. from ..db import ensure_label_table, get_connection
  13. ROOT = Path(__file__).resolve().parents[3]
  14. LABEL_FILES = (
  15. {
  16. "key": "vibration",
  17. "label": "振动异常段",
  18. "relativePath": "models/datas/reference/振动异常段.csv",
  19. },
  20. {
  21. "key": "oil_pressure",
  22. "label": "润滑油压力小时阶段段",
  23. "relativePath": "models/datas/reference/润滑油压力小时阶段段.csv",
  24. },
  25. {
  26. "key": "temperature",
  27. "label": "温度异常段",
  28. "relativePath": "_tmp_temperature/out/温度异常段.csv",
  29. },
  30. )
  31. SEVERITY = {"轻微": 1, "异常": 2, "严重异常": 3}
  32. VIBRATION_SIDE_TO_METRIC = {"联轴器端": "YSJ_10", "链轮端": "YSJ_11"}
  33. INSERT_SQL = """
  34. INSERT INTO compressor_label (
  35. source_type, origin, target_type, target_id, target_key, channel_key, segment_key,
  36. unit, device_code, device_part, device_point, metric_key, metric_label,
  37. start_time, end_time, algorithm_start, algorithm_end,
  38. label_type, label_subtype, severity, confirm_status, usable_for_train,
  39. source_file, source_row, algorithm_version, features_json
  40. ) VALUES (
  41. %s, %s, %s, %s, %s, %s, %s,
  42. %s, %s, %s, %s, %s, %s,
  43. %s, %s, %s, %s,
  44. %s, %s, %s, %s, %s,
  45. %s, %s, %s, %s
  46. )
  47. ON DUPLICATE KEY UPDATE
  48. label_type = IF(compressor_label.origin = 'manual', compressor_label.label_type, VALUES(label_type)),
  49. label_subtype = IF(compressor_label.origin = 'manual', compressor_label.label_subtype, VALUES(label_subtype)),
  50. severity = IF(compressor_label.origin = 'manual', compressor_label.severity, VALUES(severity)),
  51. confirm_status = IF(compressor_label.origin = 'manual', compressor_label.confirm_status, VALUES(confirm_status)),
  52. usable_for_train = IF(compressor_label.origin = 'manual', compressor_label.usable_for_train, VALUES(usable_for_train)),
  53. features_json = IF(compressor_label.origin = 'manual', compressor_label.features_json, VALUES(features_json)),
  54. updated_at = CURRENT_TIMESTAMP
  55. """
  56. ProgressCallback = Callable[[int, str], None]
  57. def _now() -> str:
  58. return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  59. def read_csv(path: Path) -> list[dict[str, str]]:
  60. if not path.exists():
  61. return []
  62. with path.open("r", encoding="utf-8-sig", newline="") as handle:
  63. return list(csv.DictReader(handle))
  64. def parse_unit(raw: str) -> str | None:
  65. match = re.match(r"\s*(\d+)\s*号机", raw or "")
  66. return match.group(1) if match else None
  67. def parse_time(raw: str) -> datetime | None:
  68. text = (raw or "").strip()
  69. if not text:
  70. return None
  71. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
  72. try:
  73. return datetime.strptime(text, fmt)
  74. except ValueError:
  75. continue
  76. return None
  77. def time_text(value: datetime) -> str:
  78. return value.strftime("%Y-%m-%d %H:%M:%S")
  79. def metric_label(site_points: dict[str, str], unit: str, metric: str) -> str:
  80. return site_points.get(f"YSJ{unit}_{metric.split('_')[-1]}", metric)
  81. def confirm_status(row: dict[str, str]) -> str:
  82. return "已确认" if (row.get("是否形成确认等级") or "").strip() == "是" else "未确认"
  83. def make_segment(
  84. *,
  85. source_type: str,
  86. unit: str,
  87. metric: str,
  88. start: datetime,
  89. end: datetime,
  90. label_type: str,
  91. label_subtype: str | None,
  92. row: dict[str, str],
  93. source_file: str,
  94. site_points: dict[str, str],
  95. ) -> tuple[Any, ...]:
  96. device_code = f"{unit}#"
  97. target_key = f"pks:{unit}:{device_code}:{metric}"
  98. source_row = (row.get("周期编号") or "").strip()
  99. segment_key = hashlib.md5(
  100. f"{source_type}|{unit}|{metric}|{source_row}|{time_text(start)}|{time_text(end)}".encode("utf-8")
  101. ).hexdigest()
  102. label = metric_label(site_points, unit, metric)
  103. return (
  104. source_type,
  105. "algorithm",
  106. "pks",
  107. None,
  108. target_key,
  109. target_key,
  110. segment_key,
  111. unit,
  112. device_code,
  113. (row.get("机组") or "").strip(),
  114. label,
  115. metric,
  116. label,
  117. time_text(start),
  118. time_text(end),
  119. time_text(start),
  120. time_text(end),
  121. label_type,
  122. label_subtype,
  123. SEVERITY.get(label_type),
  124. confirm_status(row),
  125. 1,
  126. source_file,
  127. source_row,
  128. "csv-v1",
  129. json.dumps(row, ensure_ascii=False),
  130. )
  131. def build_rows_for_file(source_type: str, path: Path, site_points: dict[str, str]) -> list[tuple[Any, ...]]:
  132. rows: list[tuple[Any, ...]] = []
  133. source_file = str(path.relative_to(ROOT))
  134. for row in read_csv(path):
  135. unit = parse_unit(row.get("机组", ""))
  136. start = parse_time(row.get("开始小时", ""))
  137. end = parse_time(row.get("结束小时", ""))
  138. if not unit or not start or not end:
  139. continue
  140. if source_type == "vibration":
  141. side = (row.get("最强异常侧") or "").strip()
  142. metric = VIBRATION_SIDE_TO_METRIC.get(side)
  143. if not metric:
  144. continue
  145. rows.append(
  146. make_segment(
  147. source_type=source_type,
  148. unit=unit,
  149. metric=metric,
  150. start=start,
  151. end=end + timedelta(hours=1),
  152. label_type=(row.get("振动阶段") or "").strip(),
  153. label_subtype=side or None,
  154. row=row,
  155. source_file=source_file,
  156. site_points=site_points,
  157. )
  158. )
  159. elif source_type == "oil_pressure":
  160. metric = "YSJ_5" if unit in {"7", "8", "9"} else "YSJ_1"
  161. rows.append(
  162. make_segment(
  163. source_type=source_type,
  164. unit=unit,
  165. metric=metric,
  166. start=start,
  167. end=end + timedelta(hours=1),
  168. label_type=(row.get("压力阶段") or "").strip(),
  169. label_subtype="润滑油压力",
  170. row=row,
  171. source_file=source_file,
  172. site_points=site_points,
  173. )
  174. )
  175. elif source_type == "temperature":
  176. metric = (row.get("阶段点位") or "").strip()
  177. if not re.fullmatch(r"YSJ_\d+", metric):
  178. continue
  179. rows.append(
  180. make_segment(
  181. source_type=source_type,
  182. unit=unit,
  183. metric=metric,
  184. start=start,
  185. end=end + timedelta(hours=1),
  186. label_type=(row.get("温度阶段") or "").strip(),
  187. label_subtype=(row.get("阶段部位") or row.get("特征类型") or "").strip() or None,
  188. row=row,
  189. source_file=source_file,
  190. site_points=site_points,
  191. )
  192. )
  193. return [row for row in rows if row[17] in SEVERITY]
  194. def build_rows(site_points: dict[str, str]) -> list[tuple[Any, ...]]:
  195. rows: list[tuple[Any, ...]] = []
  196. for item in LABEL_FILES:
  197. path = ROOT / item["relativePath"]
  198. rows.extend(build_rows_for_file(item["key"], path, site_points))
  199. return rows
  200. def load_site_points() -> dict[str, str]:
  201. with get_connection() as connection:
  202. with connection.cursor() as cursor:
  203. cursor.execute("SELECT ItemName, ItemDescription FROM site_point")
  204. return {str(row["ItemName"]): str(row["ItemDescription"] or "") for row in cursor.fetchall()}
  205. def file_status() -> list[dict[str, Any]]:
  206. files: list[dict[str, Any]] = []
  207. for item in LABEL_FILES:
  208. path = ROOT / item["relativePath"]
  209. exists = path.exists()
  210. files.append(
  211. {
  212. "key": item["key"],
  213. "label": item["label"],
  214. "relativePath": item["relativePath"],
  215. "exists": exists,
  216. "size": path.stat().st_size if exists else None,
  217. "modifiedAt": (
  218. datetime.fromtimestamp(path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
  219. if exists
  220. else None
  221. ),
  222. }
  223. )
  224. return files
  225. def run_import(progress: ProgressCallback | None = None) -> dict[str, Any]:
  226. started = time.monotonic()
  227. ensure_label_table()
  228. if progress:
  229. progress(5, "准备")
  230. site_points = load_site_points()
  231. rows: list[tuple[Any, ...]] = []
  232. by_source: dict[str, int] = {}
  233. skipped: list[str] = []
  234. for index, item in enumerate(LABEL_FILES):
  235. path = ROOT / item["relativePath"]
  236. if progress:
  237. progress(15 + index * 18, f"读取{item['label']}")
  238. if not path.exists():
  239. skipped.append(item["relativePath"])
  240. continue
  241. file_rows = build_rows_for_file(item["key"], path, site_points)
  242. by_source[item["key"]] = len(file_rows)
  243. rows.extend(file_rows)
  244. if progress:
  245. progress(75, "写入数据库")
  246. if rows:
  247. with get_connection() as connection:
  248. with connection.cursor() as cursor:
  249. cursor.executemany(INSERT_SQL, rows)
  250. if progress:
  251. progress(90, "统计结果")
  252. with get_connection() as connection:
  253. with connection.cursor() as cursor:
  254. cursor.execute(
  255. "SELECT COUNT(*) AS n FROM compressor_label WHERE origin = 'algorithm' AND deleted_at IS NULL"
  256. )
  257. active = int(cursor.fetchone()["n"])
  258. duration_ms = int((time.monotonic() - started) * 1000)
  259. if progress:
  260. progress(100, "完成")
  261. return {
  262. "total": len(rows),
  263. "bySource": by_source,
  264. "skippedFiles": skipped,
  265. "activeLabels": active,
  266. "durationMs": duration_ms,
  267. }
  268. _job_lock = threading.Lock()
  269. _job_state: dict[str, Any] = {
  270. "running": False,
  271. "progress": 0,
  272. "stage": "空闲",
  273. "message": "",
  274. "startedAt": None,
  275. "finishedAt": None,
  276. "result": None,
  277. "error": None,
  278. }
  279. def job_status() -> dict[str, Any]:
  280. with _job_lock:
  281. return copy.deepcopy(_job_state)
  282. def _update_job(**kwargs: Any) -> None:
  283. with _job_lock:
  284. _job_state.update(kwargs)
  285. def _progress_callback(progress: int, stage: str) -> None:
  286. _update_job(progress=progress, stage=stage)
  287. def _run_job() -> None:
  288. try:
  289. result = run_import(progress=_progress_callback)
  290. _update_job(
  291. running=False,
  292. progress=100,
  293. stage="完成",
  294. result=result,
  295. finishedAt=_now(),
  296. error=None,
  297. )
  298. except Exception as error: # noqa: BLE001
  299. _update_job(
  300. running=False,
  301. stage="失败",
  302. error=str(error),
  303. finishedAt=_now(),
  304. )
  305. def start_import() -> None:
  306. with _job_lock:
  307. if _job_state["running"]:
  308. raise ValueError("导入正在进行中")
  309. _job_state.update(
  310. {
  311. "running": True,
  312. "progress": 0,
  313. "stage": "准备",
  314. "message": "",
  315. "startedAt": _now(),
  316. "finishedAt": None,
  317. "result": None,
  318. "error": None,
  319. }
  320. )
  321. threading.Thread(target=_run_job, daemon=True).start()