- 新增BI模块(src/bi/),包含数据模型、服务和控制器 - 支持数据源管理、图表创建、看板配置 - 实现多图表类型:折线图、柱状图、饼图、散点图、面积图、仪表盘、表格 - 提供REST API(/bi/)和前端API(/bi-api/)接口 - 创建响应式前端界面,支持拖拽和实时数据展示 - 默认包含运营总览、设备管理、安全监控看板 - 支持与Superset和Metabase集成 🤖 Generated with [OpenClaw](https://github.com/robocomp/openclaw)
189 lines
5.4 KiB
Python
189 lines
5.4 KiB
Python
"""
|
|
REST API 数据接入模块
|
|
支持 IoT 设备数据、手动录入和 API 批量导入
|
|
"""
|
|
from fastapi import FastAPI, HTTPException, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
from typing import List, Optional, Dict, Any
|
|
import uvicorn
|
|
import asyncio
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# 导入BI模块
|
|
from ..bi.controllers import router as bi_router
|
|
from .bi_api import router as bi_api_router
|
|
|
|
# 将BI路由添加到主应用
|
|
app.include_router(bi_router)
|
|
app.include_router(bi_api_router)
|
|
|
|
# 创建FastAPI应用
|
|
app = FastAPI(title="Water Management System Data API", version="1.0.0")
|
|
|
|
# CORS配置
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# 数据模型
|
|
class IoTData(BaseModel):
|
|
device_id: str
|
|
data_type: str # "LL", "YL", "SW", "ZD" 等
|
|
value: float
|
|
timestamp: datetime
|
|
location: str
|
|
|
|
class ManualInputData(BaseModel):
|
|
source: str
|
|
data_type: str
|
|
value: float
|
|
timestamp: datetime
|
|
operator: str
|
|
notes: Optional[str] = None
|
|
|
|
class BatchImportRequest(BaseModel):
|
|
batch_id: str
|
|
data_source: str
|
|
records: List[Dict[str, Any]]
|
|
|
|
# 数据存储(示例,实际应该用数据库)
|
|
data_store = []
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""API根路径"""
|
|
return {"message": "Water Management System API", "version": "1.0.0"}
|
|
|
|
@app.post("/api/iot/data")
|
|
async def receive_iot_data(data: IoTData):
|
|
"""接收IoT设备数据"""
|
|
try:
|
|
data_dict = data.dict()
|
|
data_store.append({
|
|
**data_dict,
|
|
"id": len(data_store) + 1,
|
|
"type": "iot"
|
|
})
|
|
return {"status": "success", "id": len(data_store), "message": "IoT data received"}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
@app.post("/api/manual/data")
|
|
async def receive_manual_data(data: ManualInputData):
|
|
"""接收手动录入数据"""
|
|
try:
|
|
data_dict = data.dict()
|
|
data_store.append({
|
|
**data_dict,
|
|
"id": len(data_store) + 1,
|
|
"type": "manual"
|
|
})
|
|
return {"status": "success", "id": len(data_store), "message": "Manual data received"}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
@app.post("/api/batch/import")
|
|
async def batch_import(request: BatchImportRequest):
|
|
"""批量导入数据"""
|
|
try:
|
|
imported_count = 0
|
|
failed_count = 0
|
|
|
|
for record in request.records:
|
|
# 验证记录
|
|
if not all(k in record for k in ['device_id', 'data_type', 'value']):
|
|
failed_count += 1
|
|
continue
|
|
|
|
# 创建数据对象
|
|
data_record = {
|
|
"device_id": record['device_id'],
|
|
"data_type": record['data_type'],
|
|
"value": float(record['value']),
|
|
"timestamp": record.get('timestamp', datetime.now()),
|
|
"location": record.get('location', 'unknown'),
|
|
"batch_id": request.batch_id,
|
|
"source": request.data_source,
|
|
"id": len(data_store) + 1,
|
|
"type": "batch"
|
|
}
|
|
|
|
data_store.append(data_record)
|
|
imported_count += 1
|
|
|
|
return {
|
|
"status": "success",
|
|
"imported_count": imported_count,
|
|
"failed_count": failed_count,
|
|
"message": f"Batch import completed: {imported_count} records imported, {failed_count} failed"
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
@app.get("/api/data/{data_type}")
|
|
async def get_data_by_type(data_type: str, limit: int = 100, offset: int = 0):
|
|
"""根据数据类型获取数据"""
|
|
filtered_data = [
|
|
item for item in data_store
|
|
if item.get('data_type') == data_type
|
|
]
|
|
|
|
return {
|
|
"data": filtered_data[offset:offset+limit],
|
|
"total": len(filtered_data),
|
|
"limit": limit,
|
|
"offset": offset
|
|
}
|
|
|
|
@app.get("/api/data/recent")
|
|
async def get_recent_data(hours: int = 24, limit: int = 100):
|
|
"""获取最近的数据"""
|
|
from datetime import timedelta
|
|
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
recent_data = [
|
|
item for item in data_store
|
|
if item.get('timestamp', datetime.now()) > cutoff_time
|
|
]
|
|
|
|
return {
|
|
"data": recent_data[-limit:],
|
|
"total": len(recent_data),
|
|
"hours": hours,
|
|
"limit": limit
|
|
}
|
|
|
|
@app.get("/api/stats")
|
|
async def get_statistics():
|
|
"""获取数据统计信息"""
|
|
stats = {
|
|
"total_records": len(data_store),
|
|
"by_type": {},
|
|
"by_device": {},
|
|
"by_hour": {}
|
|
}
|
|
|
|
for item in data_store:
|
|
data_type = item.get('data_type', 'unknown')
|
|
device_id = item.get('device_id', 'unknown')
|
|
hour = item.get('timestamp', datetime.now()).strftime('%Y-%m-%d %H:00:00')
|
|
|
|
stats['by_type'][data_type] = stats['by_type'].get(data_type, 0) + 1
|
|
stats['by_device'][device_id] = stats['by_device'].get(device_id, 0) + 1
|
|
stats['by_hour'][hour] = stats['by_hour'].get(hour, 0) + 1
|
|
|
|
return stats
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""健康检查"""
|
|
return {"status": "healthy", "timestamp": datetime.now()}
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8000) |