diagnosis_service.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. from __future__ import annotations
  2. from datetime import datetime, timezone
  3. from sqlalchemy.orm import Session
  4. from app.db.base import new_id
  5. from app.db.models.diagnosis import DiagnosisTask
  6. from app.db.repositories import diagnosis as diagnosis_repo
  7. from app.db.repositories import mine as mine_repo
  8. from app.db.repositories import monitoring as monitoring_repo
  9. from app.schemas.diagnosis import DiagnosisRunRequest
  10. from app.services.mine_service import require_mine
  11. SENSOR_SCENES = {
  12. "gas": "灾害防治",
  13. "dust": "灾害防治",
  14. "fire": "灾害防治",
  15. "co": "灾害防治",
  16. "airflow": "风量分配",
  17. }
  18. def _task_no() -> str:
  19. stamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
  20. return f"DX-{stamp}-{new_id()[:6].upper()}"
  21. def _severity_for(sensor, value: float) -> str | None:
  22. if sensor.alarm_threshold is not None and value >= sensor.alarm_threshold:
  23. return "high"
  24. if sensor.warning_threshold is not None and value >= sensor.warning_threshold:
  25. return "medium"
  26. return None
  27. def run_diagnosis(
  28. db: Session,
  29. payload: DiagnosisRunRequest,
  30. ) -> DiagnosisTask:
  31. mine = require_mine(db, payload.mine_id)
  32. task = diagnosis_repo.create_task(
  33. db,
  34. {
  35. "mine_id": mine.id,
  36. "task_no": _task_no(),
  37. "scene": payload.scene,
  38. "trigger_type": "manual",
  39. "status": "running",
  40. "started_at": datetime.now(timezone.utc),
  41. "requested_by": payload.requested_by,
  42. "model_name": "rule-engine",
  43. "input_snapshot": payload.context,
  44. },
  45. )
  46. sensors = monitoring_repo.list_sensors(db, mine.id)
  47. for sensor in sensors:
  48. scene = SENSOR_SCENES.get(sensor.sensor_type)
  49. if scene is None:
  50. continue
  51. if payload.scene != "all" and payload.scene != scene:
  52. continue
  53. if sensor.last_value is None:
  54. continue
  55. severity = _severity_for(sensor, float(sensor.last_value))
  56. if severity is None:
  57. continue
  58. result = diagnosis_repo.create_result(
  59. db,
  60. {
  61. "task_id": task.id,
  62. "defect_code": f"SENSOR-{sensor.sensor_type.upper()}-LIMIT",
  63. "defect_name": f"{sensor.name}监测值超过阈值",
  64. "scene": scene,
  65. "severity": severity,
  66. "confidence": 0.96 if severity == "high" else 0.88,
  67. "status": "open",
  68. "root_cause": "",
  69. "conclusion": (
  70. f"当前值 {sensor.last_value}{sensor.unit},"
  71. f"报警阈值 {sensor.alarm_threshold or '-'}{sensor.unit}。"
  72. ),
  73. "recommendation": "核实现场工况,完成参数复测并记录处置过程。",
  74. "related_objects": [{"type": "sensor", "id": sensor.id, "code": sensor.code}],
  75. "raw_output": {
  76. "value": sensor.last_value,
  77. "warning_threshold": sensor.warning_threshold,
  78. "alarm_threshold": sensor.alarm_threshold,
  79. },
  80. },
  81. )
  82. diagnosis_repo.create_evidence(
  83. db,
  84. {
  85. "result_id": result.id,
  86. "evidence_type": "monitoring",
  87. "source_id": sensor.id,
  88. "source_name": sensor.name,
  89. "content": f"最近监测值 {sensor.last_value}{sensor.unit}",
  90. "confidence": result.confidence,
  91. "occurred_at": sensor.last_seen_at,
  92. "metadata_json": {"sensor_code": sensor.code},
  93. },
  94. )
  95. facilities = monitoring_repo.list_facilities(db, mine.id)
  96. for facility in facilities:
  97. if payload.scene not in {"all", "通风设施"} or facility.status == "normal":
  98. continue
  99. result = diagnosis_repo.create_result(
  100. db,
  101. {
  102. "task_id": task.id,
  103. "defect_code": "FACILITY-STATUS-ABNORMAL",
  104. "defect_name": f"{facility.name}状态异常",
  105. "scene": "通风设施",
  106. "severity": "medium",
  107. "confidence": 0.9,
  108. "status": "open",
  109. "conclusion": f"设施当前状态为 {facility.status}。",
  110. "recommendation": "安排现场复核设施开闭状态、传感器状态和视频画面。",
  111. "related_objects": [
  112. {"type": "ventilation_facility", "id": facility.id, "code": facility.code}
  113. ],
  114. "raw_output": {"status": facility.status, "open_state": facility.open_state},
  115. },
  116. )
  117. diagnosis_repo.create_evidence(
  118. db,
  119. {
  120. "result_id": result.id,
  121. "evidence_type": "facility",
  122. "source_id": facility.id,
  123. "source_name": facility.name,
  124. "content": f"设施状态 {facility.status},开闭状态 {facility.open_state or '-'}",
  125. "confidence": 0.9,
  126. "occurred_at": facility.last_checked_at,
  127. "metadata_json": {},
  128. },
  129. )
  130. work_faces = mine_repo.list_work_faces(db, mine.id)
  131. for work_face in work_faces:
  132. if payload.scene not in {"all", "风量分配"}:
  133. continue
  134. planned = work_face.planned_air_volume
  135. actual = work_face.actual_air_volume
  136. if not planned or actual is None or actual >= planned * 0.9:
  137. continue
  138. severity = "high" if actual < planned * 0.8 else "medium"
  139. result = diagnosis_repo.create_result(
  140. db,
  141. {
  142. "task_id": task.id,
  143. "defect_code": "AIRFLOW-INSUFFICIENT",
  144. "defect_name": f"{work_face.name}风量不足",
  145. "scene": "风量分配",
  146. "severity": severity,
  147. "confidence": 0.92,
  148. "status": "open",
  149. "conclusion": f"实际风量 {actual},计划风量 {planned}。",
  150. "recommendation": "核查风窗开度、漏风点和通风阻力,完成风量调整后复测。",
  151. "related_objects": [
  152. {"type": "work_face", "id": work_face.id, "code": work_face.code}
  153. ],
  154. "raw_output": {"actual_air_volume": actual, "planned_air_volume": planned},
  155. },
  156. )
  157. diagnosis_repo.create_evidence(
  158. db,
  159. {
  160. "result_id": result.id,
  161. "evidence_type": "business_metric",
  162. "source_id": work_face.id,
  163. "source_name": work_face.name,
  164. "content": f"实际风量 {actual},计划风量 {planned}",
  165. "confidence": 0.92,
  166. "occurred_at": None,
  167. "metadata_json": {},
  168. },
  169. )
  170. return diagnosis_repo.update_task(
  171. db,
  172. task,
  173. {
  174. "status": "completed",
  175. "finished_at": datetime.now(timezone.utc),
  176. "model_name": "rule-engine",
  177. },
  178. )