main.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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
  8. from .services.data_service import data_service
  9. from .services.pretrain_service import pretrain_service
  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 PretrainRecordInput(BaseModel):
  49. id: int = Field(gt=0)
  50. point_name: str = Field(min_length=1)
  51. measurement_type: str = Field(min_length=1)
  52. rpm: float | None = None
  53. sample_time: str = Field(min_length=1)
  54. file_name: str = Field(default="")
  55. class LoginInput(BaseModel):
  56. username: str = Field(min_length=1)
  57. password: str = Field(min_length=1)
  58. app = FastAPI(
  59. title="压缩机故障预测波形服务",
  60. version="0.1.0",
  61. )
  62. app.add_middleware(
  63. CORSMiddleware,
  64. allow_origins=[settings.cors_origin, "http://127.0.0.1:5173"],
  65. allow_credentials=True,
  66. allow_methods=["*"],
  67. allow_headers=["*"],
  68. )
  69. def require_auth(authorization: str | None = Header(default=None)) -> str:
  70. token = (authorization or "").removeprefix("Bearer ").strip()
  71. if not validate_token(token):
  72. raise HTTPException(status_code=401, detail="未登录或登录已过期")
  73. return token
  74. @app.on_event("startup")
  75. def startup() -> None:
  76. try:
  77. ensure_annotation_table()
  78. except Exception:
  79. # 数据库不可达时跳过建表,演示模式仍可正常启动。
  80. pass
  81. @app.post("/api/login")
  82. def login(payload: LoginInput) -> dict[str, Any]:
  83. if not verify_credentials(payload.username, payload.password):
  84. raise HTTPException(status_code=401, detail="用户名或密码错误")
  85. return {"token": create_token(), "username": payload.username}
  86. @app.post("/api/logout")
  87. def logout(authorization: str | None = Header(default=None)) -> dict[str, Any]:
  88. token = (authorization or "").removeprefix("Bearer ").strip()
  89. revoke_token(token)
  90. return {"ok": True}
  91. @app.get("/api/health")
  92. def health(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  93. return data_service.health()
  94. @app.get("/api/tag-manual/labels")
  95. def tag_manual_labels() -> dict[str, Any]:
  96. try:
  97. return tag_manual_service.labels()
  98. except Exception as error:
  99. raise HTTPException(status_code=503, detail=str(error)) from error
  100. @app.get("/api/tag-manual/series")
  101. def tag_manual_series(key: str = Query(min_length=1)) -> dict[str, Any]:
  102. try:
  103. return tag_manual_service.series(key)
  104. except KeyError as error:
  105. raise HTTPException(status_code=404, detail="标签不存在") from error
  106. except Exception as error:
  107. raise HTTPException(status_code=503, detail=str(error)) from error
  108. @app.get("/api/temperature-transition/labels")
  109. def temperature_transition_labels() -> dict[str, Any]:
  110. try:
  111. return temperature_transition_service.labels()
  112. except Exception as error:
  113. raise HTTPException(status_code=503, detail=str(error)) from error
  114. @app.get("/api/temperature-transition/series")
  115. def temperature_transition_series(key: str = Query(min_length=1)) -> dict[str, Any]:
  116. try:
  117. return temperature_transition_service.series(key)
  118. except KeyError as error:
  119. raise HTTPException(status_code=404, detail="过渡标签不存在") from error
  120. except Exception as error:
  121. raise HTTPException(status_code=503, detail=str(error)) from error
  122. @app.get("/api/query-options")
  123. def query_options(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  124. try:
  125. result = data_service.query_options()
  126. # Counts are keyed by the full point_name so the UI can scope them to
  127. # the currently selected device_part.
  128. result["pretrainCounts"] = pretrain_service.recorded_counts()
  129. result["abnormalCounts"] = data_service.abnormal_counts()
  130. return result
  131. except Exception as error:
  132. raise HTTPException(status_code=503, detail=str(error)) from error
  133. @app.get("/api/time-points")
  134. def time_points(
  135. device_part: str = Query(min_length=1),
  136. device_points: list[str] = Query(default=[]),
  137. min_time: str | None = None,
  138. max_time: str | None = None,
  139. include_stopped: bool = False,
  140. min_status: int | None = Query(default=None, ge=0),
  141. status_filter: list[str] = Query(default=[]),
  142. site_points: list[str] = Query(default=[]),
  143. _auth: str = Depends(require_auth),
  144. ) -> dict[str, Any]:
  145. try:
  146. return data_service.time_points(
  147. device_part,
  148. device_points,
  149. min_time,
  150. max_time,
  151. include_stopped,
  152. min_status,
  153. status_filter,
  154. site_points,
  155. )
  156. except ValueError as error:
  157. raise HTTPException(status_code=400, detail=str(error)) from error
  158. except Exception as error:
  159. raise HTTPException(status_code=503, detail=str(error)) from error
  160. @app.get("/api/site-points")
  161. def site_points(
  162. device_part: str = Query(min_length=1),
  163. _auth: str = Depends(require_auth),
  164. ) -> dict[str, Any]:
  165. try:
  166. return data_service.site_points(device_part)
  167. except Exception as error:
  168. raise HTTPException(status_code=503, detail=str(error)) from error
  169. @app.get("/api/faults")
  170. def faults(
  171. device_part: str = Query(min_length=1),
  172. min_time: str | None = None,
  173. max_time: str | None = None,
  174. _auth: str = Depends(require_auth),
  175. ) -> dict[str, Any]:
  176. try:
  177. return data_service.faults(device_part, min_time, max_time)
  178. except ValueError as error:
  179. raise HTTPException(status_code=400, detail=str(error)) from error
  180. except Exception as error:
  181. raise HTTPException(status_code=503, detail=str(error)) from error
  182. @app.get("/api/tspluse-ruler")
  183. def tspluse_ruler(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  184. try:
  185. return data_service.tspluse_ruler()
  186. except Exception as error:
  187. raise HTTPException(status_code=503, detail=str(error)) from error
  188. @app.get("/api/alarms")
  189. def list_alarms(
  190. current_time: str | None = None,
  191. _auth: str = Depends(require_auth),
  192. ) -> dict[str, Any]:
  193. try:
  194. return data_service.list_alarms(current_time)
  195. except ValueError as error:
  196. raise HTTPException(status_code=400, detail=str(error)) from error
  197. except Exception as error:
  198. raise HTTPException(status_code=503, detail=str(error)) from error
  199. @app.get("/api/alarm-compressor-options")
  200. def alarm_compressor_options(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  201. try:
  202. return data_service.alarm_compressor_options()
  203. except Exception as error:
  204. raise HTTPException(status_code=503, detail=str(error)) from error
  205. @app.post("/api/alarm-scan")
  206. def alarm_scan(payload: AlarmScanInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  207. try:
  208. return data_service.scan_alarm(payload.model_dump())
  209. except ValueError as error:
  210. raise HTTPException(status_code=400, detail=str(error)) from error
  211. except Exception as error:
  212. raise HTTPException(status_code=503, detail=str(error)) from error
  213. @app.post("/api/pressure-angle/sync")
  214. def pressure_angle_sync(payload: PressureAngleSyncInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  215. try:
  216. return data_service.sync_pressure_angle(payload.device_part, payload.device_points)
  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/query")
  222. def pressure_angle_query(payload: PressureAngleQueryInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  223. try:
  224. return data_service.query_pressure_angle(
  225. payload.device_part,
  226. payload.device_points,
  227. payload.min_time,
  228. payload.max_time,
  229. payload.angles,
  230. )
  231. except ValueError as error:
  232. raise HTTPException(status_code=400, detail=str(error)) from error
  233. except Exception as error:
  234. raise HTTPException(status_code=503, detail=str(error)) from error
  235. @app.post("/api/wave-window")
  236. def wave_window(payload: WaveWindowInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  237. try:
  238. return data_service.wave_window(
  239. payload.devicePart,
  240. payload.devicePoints,
  241. [point.model_dump() if hasattr(point, "model_dump") else point.dict() for point in payload.points],
  242. payload.maxPoints,
  243. payload.noSampling,
  244. payload.firstCycleOnly,
  245. payload.sitePoints,
  246. )
  247. except ValueError as error:
  248. raise HTTPException(status_code=400, detail=str(error)) from error
  249. except Exception as error:
  250. raise HTTPException(status_code=503, detail=str(error)) from error
  251. @app.get("/api/wave-files/{wave_file_id}/periods/{period_number}")
  252. def period_detail(
  253. wave_file_id: int,
  254. period_number: int,
  255. _auth: str = Depends(require_auth),
  256. ) -> dict[str, Any]:
  257. try:
  258. return data_service.period_detail(wave_file_id, period_number)
  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/annotation-config")
  264. def annotation_config(_auth: str = Depends(require_auth)) -> dict[str, Any]:
  265. return data_service.annotation_config()
  266. @app.get("/api/annotations")
  267. def list_annotations(
  268. wave_file_ids: list[int] = Query(default=[]),
  269. _auth: str = Depends(require_auth),
  270. ) -> dict[str, Any]:
  271. try:
  272. return data_service.list_annotations(wave_file_ids)
  273. except ValueError as error:
  274. raise HTTPException(status_code=400, detail=str(error)) from error
  275. except Exception as error:
  276. raise HTTPException(status_code=503, detail=str(error)) from error
  277. @app.post("/api/annotations/query")
  278. def query_annotations(
  279. payload: AnnotationQueryInput,
  280. _auth: str = Depends(require_auth),
  281. ) -> dict[str, Any]:
  282. try:
  283. return data_service.list_annotations(payload.wave_file_ids)
  284. except ValueError as error:
  285. raise HTTPException(status_code=400, detail=str(error)) from error
  286. except Exception as error:
  287. raise HTTPException(status_code=503, detail=str(error)) from error
  288. @app.post("/api/annotations")
  289. def create_annotation(payload: AnnotationInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  290. try:
  291. return data_service.create_annotation(payload.model_dump())
  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.delete("/api/annotations/{annotation_id}")
  297. def delete_annotation(annotation_id: int, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  298. try:
  299. return data_service.delete_annotation(annotation_id)
  300. except ValueError as error:
  301. raise HTTPException(status_code=404, detail=str(error)) from error
  302. except Exception as error:
  303. raise HTTPException(status_code=503, detail=str(error)) from error
  304. @app.get("/api/pretrain-records")
  305. def pretrain_records_status(
  306. ids: list[int] = Query(default=[]),
  307. _auth: str = Depends(require_auth),
  308. ) -> dict[str, Any]:
  309. try:
  310. return {"recorded": pretrain_service.recorded_ids(ids)}
  311. except Exception as error:
  312. raise HTTPException(status_code=503, detail=str(error)) from error
  313. @app.post("/api/pretrain-records")
  314. def create_pretrain_record(
  315. payload: PretrainRecordInput,
  316. _auth: str = Depends(require_auth),
  317. ) -> dict[str, Any]:
  318. try:
  319. recorded, already = pretrain_service.record(payload.model_dump())
  320. except ValueError as error:
  321. raise HTTPException(status_code=400, detail=str(error)) from error
  322. except Exception as error:
  323. raise HTTPException(status_code=503, detail=str(error)) from error
  324. return {"recorded": recorded, "already": already}
  325. @app.delete("/api/pretrain-records/{file_id}")
  326. def delete_pretrain_record(file_id: int, _auth: str = Depends(require_auth)) -> dict[str, Any]:
  327. try:
  328. deleted = pretrain_service.remove(file_id)
  329. except ValueError as error:
  330. raise HTTPException(status_code=400, detail=str(error)) from error
  331. except Exception as error:
  332. raise HTTPException(status_code=503, detail=str(error)) from error
  333. if not deleted:
  334. raise HTTPException(status_code=404, detail="CSV 中不存在该记录")
  335. return {"deleted": deleted}