feat: 性能优化 + 设备总览轨迹展示 + 广播指令API
性能: SQLite WAL模式、aiohttp Session复用、TCP连接锁+空闲超时、 device_id缓存、WebSocket并发广播、API Key认证缓存、围栏N+1查询 批量化、逆地理编码并行化、新增5个DB索引、日志降级DEBUG 功能: 广播指令API(broadcast)、exclude_type低精度后端过滤、 前端设备总览Tab+多设备轨迹叠加+高亮联动+搜索+专属颜色 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -317,6 +317,81 @@ async def batch_send_command(request: Request, body: BatchCommandRequest, db: As
|
||||
)
|
||||
|
||||
|
||||
class BroadcastCommandRequest(BaseModel):
|
||||
"""Request body for broadcasting a command to all devices."""
|
||||
command_type: str = Field(default="online_cmd", max_length=30, description="指令类型")
|
||||
command_content: str = Field(..., max_length=500, description="指令内容")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/broadcast",
|
||||
response_model=APIResponse[BatchCommandResponse],
|
||||
status_code=201,
|
||||
summary="广播指令给所有设备 / Broadcast command to all devices",
|
||||
dependencies=[Depends(require_write)],
|
||||
)
|
||||
@limiter.limit(settings.RATE_LIMIT_WRITE)
|
||||
async def broadcast_command(request: Request, body: BroadcastCommandRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
向所有设备广播同一指令。尝试通过 TCP 发送给每台设备,TCP 未连接的自动跳过。
|
||||
Broadcast the same command to all devices. Attempts TCP send for each, skips those without active TCP connection.
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from app.models import Device
|
||||
|
||||
result = await db.execute(select(Device))
|
||||
devices = list(result.scalars().all())
|
||||
|
||||
if not devices:
|
||||
return APIResponse(
|
||||
message="No devices / 没有设备",
|
||||
data=BatchCommandResponse(total=0, sent=0, failed=0, results=[]),
|
||||
)
|
||||
|
||||
results = []
|
||||
for device in devices:
|
||||
if not tcp_command_service.is_device_online(device.imei):
|
||||
results.append(BatchCommandResult(
|
||||
device_id=device.id, imei=device.imei,
|
||||
success=False, error="TCP not connected",
|
||||
))
|
||||
continue
|
||||
|
||||
try:
|
||||
cmd_log = await command_service.create_command(
|
||||
db,
|
||||
device_id=device.id,
|
||||
command_type=body.command_type,
|
||||
command_content=body.command_content,
|
||||
)
|
||||
await tcp_command_service.send_command(
|
||||
device.imei, body.command_type, body.command_content
|
||||
)
|
||||
cmd_log.status = "sent"
|
||||
cmd_log.sent_at = now_cst()
|
||||
await db.flush()
|
||||
await db.refresh(cmd_log)
|
||||
results.append(BatchCommandResult(
|
||||
device_id=device.id, imei=device.imei,
|
||||
success=True, command_id=cmd_log.id,
|
||||
))
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).error("Broadcast cmd failed for %s: %s", device.imei, e)
|
||||
results.append(BatchCommandResult(
|
||||
device_id=device.id, imei=device.imei,
|
||||
success=False, error="Send failed",
|
||||
))
|
||||
|
||||
sent = sum(1 for r in results if r.success)
|
||||
failed = len(results) - sent
|
||||
return APIResponse(
|
||||
message=f"Broadcast: {sent} sent, {failed} skipped (total: {len(devices)})",
|
||||
data=BatchCommandResponse(
|
||||
total=len(results), sent=sent, failed=failed, results=results,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{command_id}",
|
||||
response_model=APIResponse[CommandResponse],
|
||||
|
||||
@@ -31,6 +31,7 @@ router = APIRouter(prefix="/api/locations", tags=["Locations / 位置数据"])
|
||||
async def list_locations(
|
||||
device_id: int | None = Query(default=None, description="设备ID / Device ID"),
|
||||
location_type: str | None = Query(default=None, description="定位类型 / Location type (gps/lbs/wifi)"),
|
||||
exclude_type: str | None = Query(default=None, description="排除定位类型前缀 / Exclude location type prefix (e.g. lbs)"),
|
||||
start_time: datetime | None = Query(default=None, description="开始时间 / Start time (ISO 8601)"),
|
||||
end_time: datetime | None = Query(default=None, description="结束时间 / End time (ISO 8601)"),
|
||||
page: int = Query(default=1, ge=1, description="页码 / Page number"),
|
||||
@@ -45,6 +46,7 @@ async def list_locations(
|
||||
db,
|
||||
device_id=device_id,
|
||||
location_type=location_type,
|
||||
exclude_type=exclude_type,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
page=page,
|
||||
|
||||
Reference in New Issue
Block a user