main.py 13 KB

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