main.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. from typing import Any
  2. from fastapi import Depends, FastAPI, Header, HTTPException, Query
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from pydantic import BaseModel, Field
  5. from .auth import create_token, revoke_token, validate_token, verify_credentials
  6. from .config import settings
  7. from .db import ensure_annotation_table, ensure_label_table
  8. from .services.data_service import data_service
  9. from .services.label_import_service import file_status, job_status, start_import
  10. from .services.label_service import create_label, delete_label, delete_label_range, query_labels, update_label
  11. from .services.tag_manual_service import tag_manual_service
  12. from .services.temperature_transition_service import temperature_transition_service
  13. class TimePointInput(BaseModel):
  14. index: int = Field(ge=0)
  15. sampleTime: str
  16. files: dict[str, Any]
  17. siteValues: dict[str, float | None] = Field(default={})
  18. class WaveWindowInput(BaseModel):
  19. devicePart: str = Field(min_length=1)
  20. devicePoints: list[str]
  21. points: list[TimePointInput]
  22. maxPoints: int = Field(default=200000, ge=256, le=200000)
  23. noSampling: bool = False
  24. firstCycleOnly: bool = False
  25. sitePoints: list[str] = Field(default=[])
  26. class AnnotationInput(BaseModel):
  27. wave_file_id: int = Field(gt=0)
  28. label: str = Field(min_length=1, max_length=8)
  29. period_start: int = Field(gt=0)
  30. period_end: int = Field(gt=0)
  31. sample_index_start: int = Field(ge=0)
  32. sample_index_end: int = Field(ge=0)
  33. class AnnotationQueryInput(BaseModel):
  34. wave_file_ids: list[int] = Field(default=[])
  35. class AlarmScanInput(BaseModel):
  36. forecast_time: str = Field(min_length=1)
  37. device_code: str = Field(min_length=1)
  38. alarm_type: str = Field(min_length=1)
  39. hours: float = Field(gt=0)
  40. class PressureAngleSyncInput(BaseModel):
  41. device_part: str = Field(min_length=1)
  42. device_points: list[str] = Field(default=[])
  43. class PressureAngleQueryInput(BaseModel):
  44. device_part: str = Field(min_length=1)
  45. device_points: list[str] = Field(default=[])
  46. min_time: str | None = None
  47. max_time: str | None = None
  48. angles: list[int] = Field(default=[])
  49. class LabelInput(BaseModel):
  50. target_type: str = Field(min_length=1)
  51. target_id: int | None = None
  52. channel_key: str = Field(min_length=1)
  53. unit: str = Field(min_length=1)
  54. device_code: str | None = None
  55. device_part: str | None = None
  56. device_point: str | None = None
  57. metric_key: str | None = None
  58. metric_label: str | None = None
  59. start_time: str = Field(min_length=1)
  60. end_time: str = Field(min_length=1)
  61. label_type: str = Field(min_length=1)
  62. label_subtype: str | None = None
  63. note: str | None = None
  64. class LabelRangeInput(BaseModel):
  65. start_time: str = Field(min_length=1)
  66. end_time: str = Field(min_length=1)
  67. class LoginInput(BaseModel):
  68. username: str = Field(min_length=1)
  69. password: str = Field(min_length=1)
  70. app = FastAPI(
  71. title="压缩机故障预测波形服务",
  72. version="0.1.0",
  73. )
  74. app.add_middleware(
  75. CORSMiddleware,
  76. allow_origins=[settings.cors_origin, "http://127.0.0.1:5173"],
  77. allow_credentials=True,
  78. allow_methods=["*"],
  79. allow_headers=["*"],
  80. )
  81. def require_auth(authorization: str | None = Header(default=None)) -> str:
  82. token = (authorization or "").removeprefix("Bearer ").strip()
  83. if not validate_token(token):
  84. raise HTTPException(status_code=401, detail="未登录或登录已过期")
  85. return token
  86. @app.on_event("startup")
  87. def startup() -> None:
  88. try:
  89. ensure_annotation_table()
  90. ensure_label_table()
  91. except Exception:
  92. # 数据库不可达时跳过建表,演示模式仍可正常启动。
  93. pass
  94. @app.post("/api/login")
  95. def login(payload: LoginInput) -> dict[str, Any]:
  96. if not verify_credentials(payload.username, payload.password):
  97. raise HTTPException(status_code=401, detail="用户名或密码错误")
  98. return {"token": create_token(), "username": payload.username}
  99. @app.post("/api/logout")
  100. def logout(authorization: str | None = Header(default=None)) -> dict[str, Any]:
  101. token = (authorization or "").removeprefix("Bearer ").strip()
  102. revoke_token(token)
  103. return {"ok": True}
  104. @app.get("/api/health")
  105. def health(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  106. return data_service.health()
  107. @app.get("/api/tag-manual/labels")
  108. def tag_manual_labels() -> dict[str, Any]:
  109. try:
  110. return tag_manual_service.labels()
  111. except Exception as error:
  112. raise HTTPException(status_code=503, detail=str(error)) from error
  113. @app.get("/api/tag-manual/series")
  114. def tag_manual_series(key: str = Query(min_length=1)) -> dict[str, Any]:
  115. try:
  116. return tag_manual_service.series(key)
  117. except KeyError as error:
  118. raise HTTPException(status_code=404, detail="标签不存在") from error
  119. except Exception as error:
  120. raise HTTPException(status_code=503, detail=str(error)) from error
  121. @app.get("/api/temperature-transition/labels")
  122. def temperature_transition_labels() -> dict[str, Any]:
  123. try:
  124. return temperature_transition_service.labels()
  125. except Exception as error:
  126. raise HTTPException(status_code=503, detail=str(error)) from error
  127. @app.get("/api/temperature-transition/series")
  128. def temperature_transition_series(key: str = Query(min_length=1)) -> dict[str, Any]:
  129. try:
  130. return temperature_transition_service.series(key)
  131. except KeyError as error:
  132. raise HTTPException(status_code=404, detail="过渡标签不存在") from error
  133. except Exception as error:
  134. raise HTTPException(status_code=503, detail=str(error)) from error
  135. @app.get("/api/query-options")
  136. def query_options(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  137. try:
  138. result = data_service.query_options()
  139. # Counts are keyed by the full point_name so the UI can scope them to
  140. # the currently selected device_part.
  141. result["abnormalCounts"] = data_service.abnormal_counts()
  142. return result
  143. except Exception as error:
  144. raise HTTPException(status_code=503, detail=str(error)) from error
  145. @app.get("/api/time-points")
  146. def time_points(
  147. device_part: str = Query(min_length=1),
  148. device_points: list[str] = Query(default=[]),
  149. min_time: str | None = None,
  150. max_time: str | None = None,
  151. include_stopped: bool = False,
  152. min_status: int | None = Query(default=None, ge=0),
  153. status_filter: list[str] = Query(default=[]),
  154. site_points: list[str] = Query(default=[]),
  155. _auth: str = Depends(require_auth),
  156. ) -> dict[str, Any]:
  157. try:
  158. return data_service.time_points(
  159. device_part,
  160. device_points,
  161. min_time,
  162. max_time,
  163. include_stopped,
  164. min_status,
  165. status_filter,
  166. site_points,
  167. )
  168. except ValueError as error:
  169. raise HTTPException(status_code=400, detail=str(error)) from error
  170. except Exception as error:
  171. raise HTTPException(status_code=503, detail=str(error)) from error
  172. @app.get("/api/site-points")
  173. def site_points(
  174. device_part: str = Query(min_length=1),
  175. _auth: str = Depends(require_auth),
  176. ) -> dict[str, Any]:
  177. try:
  178. return data_service.site_points(device_part)
  179. except Exception as error:
  180. raise HTTPException(status_code=503, detail=str(error)) from error
  181. @app.get("/api/faults")
  182. def faults(
  183. device_part: str = Query(min_length=1),
  184. min_time: str | None = None,
  185. max_time: str | None = None,
  186. _auth: str = Depends(require_auth),
  187. ) -> dict[str, Any]:
  188. try:
  189. return data_service.faults(device_part, min_time, max_time)
  190. except ValueError as error:
  191. raise HTTPException(status_code=400, detail=str(error)) from error
  192. except Exception as error:
  193. raise HTTPException(status_code=503, detail=str(error)) from error
  194. @app.get("/api/tspluse-ruler")
  195. def tspluse_ruler(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  196. try:
  197. return data_service.tspluse_ruler()
  198. except Exception as error:
  199. raise HTTPException(status_code=503, detail=str(error)) from error
  200. @app.get("/api/alarms")
  201. def list_alarms(
  202. current_time: str | None = None,
  203. _auth: str = Depends(require_auth),
  204. ) -> dict[str, Any]:
  205. try:
  206. return data_service.list_alarms(current_time)
  207. except ValueError as error:
  208. raise HTTPException(status_code=400, detail=str(error)) from error
  209. except Exception as error:
  210. raise HTTPException(status_code=503, detail=str(error)) from error
  211. @app.get("/api/alarm-compressor-options")
  212. def alarm_compressor_options(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  213. try:
  214. return data_service.alarm_compressor_options()
  215. except Exception as error:
  216. raise HTTPException(status_code=503, detail=str(error)) from error
  217. @app.post("/api/alarm-scan")
  218. def alarm_scan(payload: AlarmScanInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  219. try:
  220. return data_service.scan_alarm(payload.model_dump())
  221. except ValueError as error:
  222. raise HTTPException(status_code=400, detail=str(error)) from error
  223. except Exception as error:
  224. raise HTTPException(status_code=503, detail=str(error)) from error
  225. @app.post("/api/pressure-angle/sync")
  226. def pressure_angle_sync(payload: PressureAngleSyncInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  227. try:
  228. return data_service.sync_pressure_angle(payload.device_part, payload.device_points)
  229. except ValueError as error:
  230. raise HTTPException(status_code=400, detail=str(error)) from error
  231. except Exception as error:
  232. raise HTTPException(status_code=503, detail=str(error)) from error
  233. @app.post("/api/pressure-angle/query")
  234. def pressure_angle_query(payload: PressureAngleQueryInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  235. try:
  236. return data_service.query_pressure_angle(
  237. payload.device_part,
  238. payload.device_points,
  239. payload.min_time,
  240. payload.max_time,
  241. payload.angles,
  242. )
  243. except ValueError as error:
  244. raise HTTPException(status_code=400, detail=str(error)) from error
  245. except Exception as error:
  246. raise HTTPException(status_code=503, detail=str(error)) from error
  247. @app.post("/api/wave-window")
  248. def wave_window(payload: WaveWindowInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  249. try:
  250. return data_service.wave_window(
  251. payload.devicePart,
  252. payload.devicePoints,
  253. [point.model_dump() if hasattr(point, "model_dump") else point.dict() for point in payload.points],
  254. payload.maxPoints,
  255. payload.noSampling,
  256. payload.firstCycleOnly,
  257. payload.sitePoints,
  258. )
  259. except ValueError as error:
  260. raise HTTPException(status_code=400, detail=str(error)) from error
  261. except Exception as error:
  262. raise HTTPException(status_code=503, detail=str(error)) from error
  263. @app.get("/api/wave-files/{wave_file_id}/periods/{period_number}")
  264. def period_detail(
  265. wave_file_id: int,
  266. period_number: int,
  267. _auth: str = Depends(require_auth),
  268. ) -> dict[str, Any]:
  269. try:
  270. return data_service.period_detail(wave_file_id, period_number)
  271. except ValueError as error:
  272. raise HTTPException(status_code=400, detail=str(error)) from error
  273. except Exception as error:
  274. raise HTTPException(status_code=503, detail=str(error)) from error
  275. @app.get("/api/annotation-config")
  276. def annotation_config(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  277. return data_service.annotation_config()
  278. @app.get("/api/annotations")
  279. def list_annotations(
  280. wave_file_ids: list[int] = Query(default=[]),
  281. _auth: str = Depends(require_auth),
  282. ) -> dict[str, Any]:
  283. try:
  284. return data_service.list_annotations(wave_file_ids)
  285. except ValueError as error:
  286. raise HTTPException(status_code=400, detail=str(error)) from error
  287. except Exception as error:
  288. raise HTTPException(status_code=503, detail=str(error)) from error
  289. @app.post("/api/annotations/query")
  290. def query_annotations(
  291. payload: AnnotationQueryInput,
  292. _auth: str = Depends(require_auth),
  293. ) -> dict[str, Any]:
  294. try:
  295. return data_service.list_annotations(payload.wave_file_ids)
  296. except ValueError as error:
  297. raise HTTPException(status_code=400, detail=str(error)) from error
  298. except Exception as error:
  299. raise HTTPException(status_code=503, detail=str(error)) from error
  300. @app.post("/api/annotations")
  301. def create_annotation(payload: AnnotationInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  302. try:
  303. return data_service.create_annotation(payload.model_dump())
  304. except ValueError as error:
  305. raise HTTPException(status_code=400, detail=str(error)) from error
  306. except Exception as error:
  307. raise HTTPException(status_code=503, detail=str(error)) from error
  308. @app.delete("/api/annotations/{annotation_id}")
  309. def delete_annotation(annotation_id: int, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  310. try:
  311. return data_service.delete_annotation(annotation_id)
  312. except ValueError as error:
  313. raise HTTPException(status_code=404, detail=str(error)) from error
  314. except Exception as error:
  315. raise HTTPException(status_code=503, detail=str(error)) from error
  316. @app.get("/api/labels")
  317. def list_labels(
  318. target_type: str | None = None,
  319. unit: str | None = None,
  320. device_code: str | None = None,
  321. metric_keys: list[str] = Query(default=[]),
  322. channel_keys: list[str] = Query(default=[]),
  323. wave_file_ids: list[int] = Query(default=[]),
  324. start: str | None = None,
  325. end: str | None = None,
  326. _auth: str = Depends(require_auth),
  327. ) -> dict[str, Any]:
  328. try:
  329. return query_labels(
  330. target_type=target_type,
  331. unit=unit,
  332. device_code=device_code,
  333. metric_keys=metric_keys,
  334. channel_keys=channel_keys,
  335. wave_file_ids=wave_file_ids,
  336. start=start,
  337. end=end,
  338. )
  339. except Exception as error:
  340. raise HTTPException(status_code=503, detail=str(error)) from error
  341. @app.post("/api/labels")
  342. def create_label_endpoint(payload: LabelInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  343. try:
  344. return create_label(payload.model_dump())
  345. except ValueError as error:
  346. raise HTTPException(status_code=400, detail=str(error)) from error
  347. except Exception as error:
  348. raise HTTPException(status_code=503, detail=str(error)) from error
  349. @app.put("/api/labels/{label_id}")
  350. def update_label_endpoint(
  351. label_id: int,
  352. payload: LabelInput,
  353. _auth: str = Depends(require_auth),
  354. ) -> dict[str, Any]:
  355. try:
  356. return update_label(label_id, payload.model_dump())
  357. except ValueError as error:
  358. raise HTTPException(status_code=400, detail=str(error)) from error
  359. except Exception as error:
  360. raise HTTPException(status_code=503, detail=str(error)) from error
  361. @app.delete("/api/labels/{label_id}")
  362. def delete_label_endpoint(label_id: int, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  363. try:
  364. return delete_label(label_id)
  365. except ValueError as error:
  366. raise HTTPException(status_code=404, detail=str(error)) from error
  367. except Exception as error:
  368. raise HTTPException(status_code=503, detail=str(error)) from error
  369. @app.post("/api/labels/{label_id}/delete-range")
  370. def delete_label_range_endpoint(
  371. label_id: int,
  372. payload: LabelRangeInput,
  373. _auth: str = Depends(require_auth),
  374. ) -> dict[str, Any]:
  375. try:
  376. return delete_label_range(label_id, payload.start_time, payload.end_time)
  377. except ValueError as error:
  378. raise HTTPException(status_code=400, detail=str(error)) from error
  379. except Exception as error:
  380. raise HTTPException(status_code=503, detail=str(error)) from error
  381. @app.get("/api/label-import/status")
  382. def label_import_status(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  383. try:
  384. return {"files": file_status(), "job": job_status()}
  385. except Exception as error:
  386. raise HTTPException(status_code=503, detail=str(error)) from error
  387. @app.post("/api/label-import")
  388. def label_import_start(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  389. try:
  390. start_import()
  391. except ValueError as error:
  392. raise HTTPException(status_code=409, detail=str(error)) from error
  393. except Exception as error:
  394. raise HTTPException(status_code=503, detail=str(error)) from error
  395. return {"started": True}
  396. @app.get("/api/label-import/progress")
  397. def label_import_progress(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  398. try:
  399. return job_status()
  400. except Exception as error:
  401. raise HTTPException(status_code=503, detail=str(error)) from error