Initial commit: migrate badge-admin from /tmp to /home/gpsystem

via HAPI (https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-03-17 01:14:40 +00:00
commit 8a18a5ff16
61 changed files with 13106 additions and 0 deletions

288
app/routers/commands.py Normal file
View File

@@ -0,0 +1,288 @@
"""
Commands Router - 指令管理接口
API endpoints for sending commands / messages to devices and viewing command history.
"""
import math
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas import (
APIResponse,
CommandResponse,
PaginatedList,
)
from app.services import command_service, device_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")
command_content: str = Field(..., 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")
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
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
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@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"),
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 and status filters.
"""
commands, total = await command_service.get_commands(
db, device_id=device_id, 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",
)
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)
# Import tcp_manager lazily to avoid circular imports
from app.tcp_server import tcp_manager
# Check if device is connected
if device.imei not in tcp_manager.connections:
raise HTTPException(
status_code=400,
detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线",
)
# Create command log entry
command_log = await command_service.create_command(
db,
device_id=device.id,
command_type=body.command_type,
command_content=body.command_content,
)
# Send command via TCP
try:
await tcp_manager.send_command(
device.imei, body.command_type, body.command_content
)
except Exception as e:
command_log.status = "failed"
await db.flush()
await db.refresh(command_log)
raise HTTPException(
status_code=500,
detail=f"Failed to send command / 指令发送失败: {str(e)}",
)
command_log.status = "sent"
await db.flush()
await db.refresh(command_log)
return APIResponse(
message="Command sent successfully / 指令发送成功",
data=CommandResponse.model_validate(command_log),
)
@router.post(
"/message",
response_model=APIResponse[CommandResponse],
status_code=201,
summary="发送留言 / Send message to device (0x82)",
)
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)
from app.tcp_server import tcp_manager
if device.imei not in tcp_manager.connections:
raise HTTPException(
status_code=400,
detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线",
)
# Create command log for the message
command_log = await command_service.create_command(
db,
device_id=device.id,
command_type="message",
command_content=body.message,
)
try:
await tcp_manager.send_message(device.imei, body.message)
except Exception as e:
command_log.status = "failed"
await db.flush()
await db.refresh(command_log)
raise HTTPException(
status_code=500,
detail=f"Failed to send message / 留言发送失败: {str(e)}",
)
command_log.status = "sent"
await db.flush()
await db.refresh(command_log)
return APIResponse(
message="Message sent successfully / 留言发送成功",
data=CommandResponse.model_validate(command_log),
)
@router.post(
"/tts",
response_model=APIResponse[CommandResponse],
status_code=201,
summary="语音下发 / Send TTS voice broadcast to device",
)
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)
from app.tcp_server import tcp_manager
if device.imei not in tcp_manager.connections:
raise HTTPException(
status_code=400,
detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线",
)
tts_command = f"TTS,{body.text}"
# Create command log entry
command_log = await command_service.create_command(
db,
device_id=device.id,
command_type="tts",
command_content=tts_command,
)
try:
await tcp_manager.send_command(device.imei, "tts", tts_command)
except Exception as e:
command_log.status = "failed"
await db.flush()
await db.refresh(command_log)
raise HTTPException(
status_code=500,
detail=f"Failed to send TTS / 语音下发失败: {str(e)}",
)
command_log.status = "sent"
await db.flush()
await db.refresh(command_log)
return APIResponse(
message="TTS sent successfully / 语音下发成功",
data=CommandResponse.model_validate(command_log),
)
@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))