feat: add project-scoped operations
This commit is contained in:
@@ -48,6 +48,7 @@ def _project_response(project: models.Project) -> AdminProjectResponse:
|
||||
description=project.description,
|
||||
gs_workspace=project.gs_workspace,
|
||||
map_extent=project.map_extent,
|
||||
map_config=getattr(project, "map_config", {}) or {},
|
||||
status=project.status,
|
||||
created_at=project.created_at,
|
||||
updated_at=project.updated_at,
|
||||
@@ -274,6 +275,7 @@ async def create_admin_project(
|
||||
description=payload.description,
|
||||
gs_workspace=payload.gs_workspace,
|
||||
map_extent=payload.map_extent,
|
||||
map_config=payload.map_config.model_dump(exclude_unset=True),
|
||||
status=payload.status,
|
||||
creator_user_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -44,6 +44,7 @@ async def get_project_metadata(
|
||||
description=project.description,
|
||||
gs_workspace=project.gs_workspace,
|
||||
map_extent=project.map_extent,
|
||||
map_config=getattr(project, "map_config", {}) or {},
|
||||
status=project.status,
|
||||
project_role=ctx.project_role,
|
||||
)
|
||||
|
||||
@@ -319,6 +319,7 @@ async def provision_project(
|
||||
description=project.description,
|
||||
gs_workspace=project.gs_workspace,
|
||||
map_extent=project.map_extent,
|
||||
map_config=getattr(project, "map_config", {}) or {},
|
||||
status=project.status,
|
||||
created_at=project.created_at,
|
||||
updated_at=project.updated_at,
|
||||
|
||||
@@ -1,19 +1,60 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.domain.schemas.place_search import PlaceSearchRequest, PlaceSearchResponse
|
||||
from app.auth.project_dependencies import ProjectContext, get_metadata_repository, get_project_context
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
from app.domain.schemas.project_map_config import PlaceSearchConfig, ProjectMapConfig
|
||||
|
||||
from app.domain.schemas.place_search import PlaceSearchItem, PlaceSearchRequest, PlaceSearchResponse, ReverseGeocodingRequest
|
||||
from app.services.place_search import PlaceSearchError, search_tianditu_places
|
||||
from app.services.reverse_geocoding import reverse_geocode_tianditu
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def get_place_search_config(
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> PlaceSearchConfig:
|
||||
project = await metadata_repo.get_project_by_id(ctx.project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
try:
|
||||
config = ProjectMapConfig.model_validate(project.map_config or {}).place_search
|
||||
except ValidationError:
|
||||
raise HTTPException(status_code=503, detail="当前项目地名搜索配置无效") from None
|
||||
if config is None or not config.enabled:
|
||||
raise HTTPException(status_code=503, detail="当前项目暂未配置地名搜索")
|
||||
return config
|
||||
|
||||
|
||||
@router.post(
|
||||
"/place-search-requests",
|
||||
response_model=PlaceSearchResponse,
|
||||
summary="Search Fengxian places",
|
||||
description="调用天地图地名搜索,固定上海市奉贤区范围,只返回前 10 个可定位候选,不提供分页。",
|
||||
summary="Search project places",
|
||||
description="调用天地图地名搜索,按当前项目 map_config.place_search 的行政区范围,只返回前 10 个可定位候选,不提供分页。",
|
||||
)
|
||||
async def tianditu_place_search(request: PlaceSearchRequest) -> PlaceSearchResponse:
|
||||
async def tianditu_place_search(
|
||||
request: PlaceSearchRequest,
|
||||
config: PlaceSearchConfig = Depends(get_place_search_config),
|
||||
) -> PlaceSearchResponse:
|
||||
try:
|
||||
return await search_tianditu_places(request)
|
||||
return await search_tianditu_places(request, config=config)
|
||||
except PlaceSearchError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/reverse-geocoding-requests",
|
||||
response_model=PlaceSearchItem,
|
||||
summary="Reverse geocode a project map point",
|
||||
description="查询 EPSG:4326 地图取点的地名和地址,复用当前项目的地名搜索配置并保留输入坐标。",
|
||||
)
|
||||
async def tianditu_reverse_geocode(
|
||||
request: ReverseGeocodingRequest,
|
||||
config: PlaceSearchConfig = Depends(get_place_search_config),
|
||||
) -> PlaceSearchItem:
|
||||
try:
|
||||
return await reverse_geocode_tianditu(request, config=config)
|
||||
except PlaceSearchError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
@@ -34,6 +34,7 @@ async def get_project_info_endpoint(
|
||||
description=project_detail.description,
|
||||
gs_workspace=project_detail.gs_workspace,
|
||||
map_extent=project_detail.map_extent,
|
||||
map_config=getattr(project_detail, "map_config", {}) or {},
|
||||
status=project_detail.status,
|
||||
project_role="viewer", # Default role for public access
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
from psycopg import AsyncConnection
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from app.domain.schemas.scada import ScadaLatestReadingsResponse
|
||||
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
||||
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
||||
from .dependencies import get_postgres_connection, get_timescale_connection
|
||||
@@ -96,6 +97,54 @@ async def get_scada_by_ids_time_range(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/timeseries/scada-readings/latest",
|
||||
response_model=ScadaLatestReadingsResponse,
|
||||
summary="获取SCADA设备最新数据",
|
||||
)
|
||||
async def get_latest_scada_readings(
|
||||
limit: int = Query(100, ge=1, le=500, description="返回设备数上限"),
|
||||
offset: int = Query(0, ge=0, description="设备目录偏移量"),
|
||||
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
|
||||
) -> ScadaLatestReadingsResponse:
|
||||
"""Read one latest persisted row per project device without applying a time window."""
|
||||
devices = await ScadaInfoRepository.get_scadas(postgres_conn)
|
||||
selected_devices = devices[offset : offset + limit]
|
||||
latest_by_device = await ScadaRepository.get_latest_scada_readings(
|
||||
conn,
|
||||
[device["device_id"] for device in selected_devices],
|
||||
)
|
||||
|
||||
items = []
|
||||
for device in selected_devices:
|
||||
reading = latest_by_device.get(device["device_id"])
|
||||
items.append(
|
||||
{
|
||||
"device_id": device["device_id"],
|
||||
"device_type": device["device_type"],
|
||||
"measurement_unit": device["measurement_unit"],
|
||||
"transmission_mode": device["transmission_mode"],
|
||||
"transmission_frequency": device["transmission_frequency"],
|
||||
"observed_at": reading["time"] if reading else None,
|
||||
"monitored_value": reading["monitored_value"] if reading else None,
|
||||
"cleaned_value": reading["cleaned_value"] if reading else None,
|
||||
}
|
||||
)
|
||||
|
||||
observed_times = [item["observed_at"] for item in items if item["observed_at"] is not None]
|
||||
return ScadaLatestReadingsResponse.model_validate(
|
||||
{
|
||||
"items": items,
|
||||
"total": len(devices),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"observed_count": len(observed_times),
|
||||
"latest_observed_at": max(observed_times) if observed_times else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/timeseries/scada-readings/fields", summary="按设备ID、字段和时间范围查询SCADA数据"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user