2026-03-27 10:19:34 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Commands Router - 指令管理接口
|
|
|
|
|
|
API endpoints for sending commands / messages to devices and viewing command history.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import math
|
2026-03-27 13:04:11 +00:00
|
|
|
|
from app.config import now_cst
|
2026-03-27 10:19:34 +00:00
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
|
|
from app.database import get_db
|
|
|
|
|
|
from app.config import settings
|
|
|
|
|
|
from app.extensions import limiter
|
|
|
|
|
|
from app.schemas import (
|
|
|
|
|
|
APIResponse,
|
|
|
|
|
|
BatchCommandRequest,
|
|
|
|
|
|
BatchCommandResponse,
|
|
|
|
|
|
BatchCommandResult,
|
|
|
|
|
|
CommandResponse,
|
|
|
|
|
|
PaginatedList,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.dependencies import require_write
|
|
|
|
|
|
from app.services import command_service, device_service
|
|
|
|
|
|
from app.services import tcp_command_service
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/commands", tags=["Commands / 指令管理"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Request schemas specific to this router
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendCommandRequest(BaseModel):
|
|
|
|
|
|
"""Request body for sending a command to a device."""
|
|
|
|
|
|
device_id: int | None = Field(None, description="设备ID / Device ID (provide device_id or imei)")
|
|
|
|
|
|
imei: str | None = Field(None, description="IMEI号 / IMEI number (provide device_id or imei)")
|
|
|
|
|
|
command_type: str = Field(..., max_length=30, description="指令类型 / Command type (e.g. online_cmd)")
|
|
|
|
|
|
command_content: str = Field(..., max_length=500, description="指令内容 / Command content")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendMessageRequest(BaseModel):
|
|
|
|
|
|
"""Request body for sending a message (0x82) to a device."""
|
|
|
|
|
|
device_id: int | None = Field(None, description="设备ID / Device ID (provide device_id or imei)")
|
|
|
|
|
|
imei: str | None = Field(None, description="IMEI号 / IMEI number (provide device_id or imei)")
|
|
|
|
|
|
message: str = Field(..., max_length=500, description="消息内容 / Message content")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendTTSRequest(BaseModel):
|
|
|
|
|
|
"""Request body for sending a TTS voice broadcast to a device."""
|
|
|
|
|
|
device_id: int | None = Field(None, description="设备ID / Device ID (provide device_id or imei)")
|
|
|
|
|
|
imei: str | None = Field(None, description="IMEI号 / IMEI number (provide device_id or imei)")
|
|
|
|
|
|
text: str = Field(..., min_length=1, max_length=200, description="语音播报文本 / TTS text content")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Helpers
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolve_device(
|
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
|
device_id: int | None,
|
|
|
|
|
|
imei: str | None,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Resolve a device from either device_id or imei. Returns the Device ORM instance."""
|
|
|
|
|
|
if device_id is None and imei is None:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail="Either device_id or imei must be provided / 必须提供 device_id 或 imei",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if device_id is not None:
|
|
|
|
|
|
device = await device_service.get_device(db, device_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
device = await device_service.get_device_by_imei(db, imei)
|
|
|
|
|
|
|
|
|
|
|
|
if device is None:
|
|
|
|
|
|
identifier = f"ID={device_id}" if device_id else f"IMEI={imei}"
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=404,
|
|
|
|
|
|
detail=f"Device {identifier} not found / 未找到设备 {identifier}",
|
|
|
|
|
|
)
|
|
|
|
|
|
return device
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _send_to_device(
|
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
|
device,
|
|
|
|
|
|
command_type: str,
|
|
|
|
|
|
command_content: str,
|
|
|
|
|
|
executor,
|
|
|
|
|
|
success_msg: str,
|
|
|
|
|
|
fail_msg: str,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Common logic for sending command/message/tts to a device via TCP.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
executor : async callable
|
|
|
|
|
|
The actual send function, e.g. tcp_command_service.send_command(...)
|
|
|
|
|
|
"""
|
|
|
|
|
|
command_log = await command_service.create_command(
|
|
|
|
|
|
db,
|
|
|
|
|
|
device_id=device.id,
|
|
|
|
|
|
command_type=command_type,
|
|
|
|
|
|
command_content=command_content,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-03-31 05:01:04 +00:00
|
|
|
|
result = await executor()
|
2026-03-27 10:19:34 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logging.getLogger(__name__).error("Command send failed: %s", e)
|
|
|
|
|
|
command_log.status = "failed"
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
await db.refresh(command_log)
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail=fail_msg,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-31 05:01:04 +00:00
|
|
|
|
if result is False:
|
|
|
|
|
|
command_log.status = "failed"
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
await db.refresh(command_log)
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail=f"Device {device.imei} TCP not connected / 设备 {device.imei} TCP未连接,请等待设备重连",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-27 10:19:34 +00:00
|
|
|
|
command_log.status = "sent"
|
2026-03-27 13:04:11 +00:00
|
|
|
|
command_log.sent_at = now_cst()
|
2026-03-27 10:19:34 +00:00
|
|
|
|
await db.flush()
|
|
|
|
|
|
await db.refresh(command_log)
|
|
|
|
|
|
|
|
|
|
|
|
return APIResponse(
|
|
|
|
|
|
message=success_msg,
|
|
|
|
|
|
data=CommandResponse.model_validate(command_log),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Endpoints
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 10:11:33 +00:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"/stats",
|
|
|
|
|
|
response_model=APIResponse[dict],
|
|
|
|
|
|
summary="指令统计 / Command statistics",
|
|
|
|
|
|
)
|
|
|
|
|
|
async def command_stats(
|
|
|
|
|
|
days: int = Query(default=7, ge=1, le=90, description="趋势天数"),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
指令统计:总数、按状态分布、按类型分布、成功率、按天趋势。
|
|
|
|
|
|
Command stats: total, by status, by type, success rate, daily trend.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
|
from app.models import CommandLog
|
|
|
|
|
|
|
|
|
|
|
|
total = (await db.execute(select(func.count(CommandLog.id)))).scalar() or 0
|
|
|
|
|
|
|
|
|
|
|
|
# By status
|
|
|
|
|
|
status_result = await db.execute(
|
|
|
|
|
|
select(CommandLog.status, func.count(CommandLog.id))
|
|
|
|
|
|
.group_by(CommandLog.status)
|
|
|
|
|
|
)
|
|
|
|
|
|
by_status = {row[0]: row[1] for row in status_result.all()}
|
|
|
|
|
|
|
|
|
|
|
|
# By type
|
|
|
|
|
|
type_result = await db.execute(
|
|
|
|
|
|
select(CommandLog.command_type, func.count(CommandLog.id))
|
|
|
|
|
|
.group_by(CommandLog.command_type)
|
|
|
|
|
|
)
|
|
|
|
|
|
by_type = {row[0]: row[1] for row in type_result.all()}
|
|
|
|
|
|
|
|
|
|
|
|
# Success rate
|
|
|
|
|
|
sent = by_status.get("sent", 0) + by_status.get("success", 0)
|
|
|
|
|
|
failed = by_status.get("failed", 0)
|
|
|
|
|
|
total_attempted = sent + failed
|
|
|
|
|
|
success_rate = round(sent / total_attempted * 100, 1) if total_attempted else 0
|
|
|
|
|
|
|
|
|
|
|
|
# Daily trend
|
|
|
|
|
|
now = now_cst()
|
|
|
|
|
|
cutoff = now - timedelta(days=days)
|
|
|
|
|
|
trend_result = await db.execute(
|
|
|
|
|
|
select(
|
|
|
|
|
|
func.date(CommandLog.created_at).label("day"),
|
|
|
|
|
|
func.count(CommandLog.id),
|
|
|
|
|
|
)
|
|
|
|
|
|
.where(CommandLog.created_at >= cutoff)
|
|
|
|
|
|
.group_by("day").order_by("day")
|
|
|
|
|
|
)
|
|
|
|
|
|
daily_trend = {str(row[0]): row[1] for row in trend_result.all()}
|
|
|
|
|
|
|
|
|
|
|
|
# Today
|
|
|
|
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
|
|
today_count = (await db.execute(
|
|
|
|
|
|
select(func.count(CommandLog.id)).where(CommandLog.created_at >= today_start)
|
|
|
|
|
|
)).scalar() or 0
|
|
|
|
|
|
|
|
|
|
|
|
return APIResponse(data={
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"today": today_count,
|
|
|
|
|
|
"by_status": by_status,
|
|
|
|
|
|
"by_type": by_type,
|
|
|
|
|
|
"success_rate": success_rate,
|
|
|
|
|
|
"daily_trend": daily_trend,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-27 10:19:34 +00:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"",
|
|
|
|
|
|
response_model=APIResponse[PaginatedList[CommandResponse]],
|
|
|
|
|
|
summary="获取指令历史 / List command history",
|
|
|
|
|
|
)
|
|
|
|
|
|
async def list_commands(
|
|
|
|
|
|
device_id: int | None = Query(default=None, description="设备ID / Device ID"),
|
|
|
|
|
|
command_type: str | None = Query(default=None, description="指令类型 / Command type (online_cmd/message/tts)"),
|
|
|
|
|
|
status: str | None = Query(default=None, description="指令状态 / Command status (pending/sent/success/failed)"),
|
|
|
|
|
|
page: int = Query(default=1, ge=1, description="页码 / Page number"),
|
|
|
|
|
|
page_size: int = Query(default=20, ge=1, le=100, description="每页数量 / Items per page"),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取指令历史记录,支持按设备、指令类型和状态过滤。
|
|
|
|
|
|
List command history with optional device, command type and status filters.
|
|
|
|
|
|
"""
|
|
|
|
|
|
commands, total = await command_service.get_commands(
|
|
|
|
|
|
db, device_id=device_id, command_type=command_type, status=status, page=page, page_size=page_size
|
|
|
|
|
|
)
|
|
|
|
|
|
return APIResponse(
|
|
|
|
|
|
data=PaginatedList(
|
|
|
|
|
|
items=[CommandResponse.model_validate(c) for c in commands],
|
|
|
|
|
|
total=total,
|
|
|
|
|
|
page=page,
|
|
|
|
|
|
page_size=page_size,
|
|
|
|
|
|
total_pages=math.ceil(total / page_size) if total else 0,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/send",
|
|
|
|
|
|
response_model=APIResponse[CommandResponse],
|
|
|
|
|
|
status_code=201,
|
|
|
|
|
|
summary="发送指令 / Send command to device",
|
|
|
|
|
|
dependencies=[Depends(require_write)],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def send_command(body: SendCommandRequest, db: AsyncSession = Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
向设备发送指令(通过TCP连接下发)。
|
|
|
|
|
|
Send a command to a device via the TCP connection.
|
|
|
|
|
|
Requires the device to be online.
|
|
|
|
|
|
"""
|
|
|
|
|
|
device = await _resolve_device(db, body.device_id, body.imei)
|
|
|
|
|
|
return await _send_to_device(
|
|
|
|
|
|
db, device,
|
|
|
|
|
|
command_type=body.command_type,
|
|
|
|
|
|
command_content=body.command_content,
|
|
|
|
|
|
executor=lambda: tcp_command_service.send_command(
|
|
|
|
|
|
device.imei, body.command_type, body.command_content
|
|
|
|
|
|
),
|
|
|
|
|
|
success_msg="Command sent successfully / 指令发送成功",
|
|
|
|
|
|
fail_msg="Failed to send command / 指令发送失败",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/message",
|
|
|
|
|
|
response_model=APIResponse[CommandResponse],
|
|
|
|
|
|
status_code=201,
|
|
|
|
|
|
summary="发送留言 / Send message to device (0x82)",
|
|
|
|
|
|
dependencies=[Depends(require_write)],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def send_message(body: SendMessageRequest, db: AsyncSession = Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
向设备发送留言消息(协议号 0x82)。
|
|
|
|
|
|
Send a text message to a device using protocol 0x82.
|
|
|
|
|
|
"""
|
|
|
|
|
|
device = await _resolve_device(db, body.device_id, body.imei)
|
|
|
|
|
|
return await _send_to_device(
|
|
|
|
|
|
db, device,
|
|
|
|
|
|
command_type="message",
|
|
|
|
|
|
command_content=body.message,
|
|
|
|
|
|
executor=lambda: tcp_command_service.send_message(device.imei, body.message),
|
|
|
|
|
|
success_msg="Message sent successfully / 留言发送成功",
|
|
|
|
|
|
fail_msg="Failed to send message / 留言发送失败",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/tts",
|
|
|
|
|
|
response_model=APIResponse[CommandResponse],
|
|
|
|
|
|
status_code=201,
|
|
|
|
|
|
summary="语音下发 / Send TTS voice broadcast to device",
|
|
|
|
|
|
dependencies=[Depends(require_write)],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def send_tts(body: SendTTSRequest, db: AsyncSession = Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
向设备发送 TTS 语音播报(通过 0x80 在线指令,TTS 命令格式)。
|
|
|
|
|
|
Send a TTS voice broadcast to a device via online command (0x80).
|
|
|
|
|
|
The device will use its built-in TTS engine to speak the text aloud.
|
|
|
|
|
|
"""
|
|
|
|
|
|
device = await _resolve_device(db, body.device_id, body.imei)
|
|
|
|
|
|
tts_command = f"TTS,{body.text}"
|
|
|
|
|
|
return await _send_to_device(
|
|
|
|
|
|
db, device,
|
|
|
|
|
|
command_type="tts",
|
|
|
|
|
|
command_content=tts_command,
|
|
|
|
|
|
executor=lambda: tcp_command_service.send_command(
|
|
|
|
|
|
device.imei, "tts", tts_command
|
|
|
|
|
|
),
|
|
|
|
|
|
success_msg="TTS sent successfully / 语音下发成功",
|
|
|
|
|
|
fail_msg="Failed to send TTS / 语音下发失败",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/batch",
|
|
|
|
|
|
response_model=APIResponse[BatchCommandResponse],
|
|
|
|
|
|
status_code=201,
|
|
|
|
|
|
summary="批量发送指令 / Batch send command to multiple devices",
|
|
|
|
|
|
dependencies=[Depends(require_write)],
|
|
|
|
|
|
)
|
|
|
|
|
|
@limiter.limit(settings.RATE_LIMIT_WRITE)
|
|
|
|
|
|
async def batch_send_command(request: Request, body: BatchCommandRequest, db: AsyncSession = Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
向多台设备批量发送同一指令,最多100台。
|
|
|
|
|
|
Send the same command to multiple devices (up to 100). Skips offline devices.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Resolve devices in single query (mutual exclusion validated by schema)
|
|
|
|
|
|
if body.device_ids:
|
|
|
|
|
|
devices = await device_service.get_devices_by_ids(db, body.device_ids)
|
|
|
|
|
|
else:
|
|
|
|
|
|
devices = await device_service.get_devices_by_imeis(db, body.imeis)
|
|
|
|
|
|
|
|
|
|
|
|
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="Device offline",
|
|
|
|
|
|
))
|
|
|
|
|
|
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"
|
2026-03-27 13:04:11 +00:00
|
|
|
|
cmd_log.sent_at = now_cst()
|
2026-03-27 10:19:34 +00:00
|
|
|
|
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("Batch 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"Batch command: {sent} sent, {failed} failed",
|
|
|
|
|
|
data=BatchCommandResponse(
|
|
|
|
|
|
total=len(results), sent=sent, failed=failed, results=results,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 09:41:09 +00:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-27 10:19:34 +00:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"/{command_id}",
|
|
|
|
|
|
response_model=APIResponse[CommandResponse],
|
|
|
|
|
|
summary="获取指令详情 / Get command details",
|
|
|
|
|
|
)
|
|
|
|
|
|
async def get_command(command_id: int, db: AsyncSession = Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
按ID获取指令详情。
|
|
|
|
|
|
Get command log details by ID.
|
|
|
|
|
|
"""
|
|
|
|
|
|
command = await command_service.get_command(db, command_id)
|
|
|
|
|
|
if command is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Command {command_id} not found / 未找到指令{command_id}")
|
|
|
|
|
|
return APIResponse(data=CommandResponse.model_validate(command))
|