main.py 15 KB

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