first commit

This commit is contained in:
jeremygan2021
2025-11-16 17:21:25 +08:00
commit a2682dc040
46 changed files with 5976 additions and 0 deletions

8
api/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
from fastapi import APIRouter
from api import devices, contents
api_router = APIRouter()
# 注册所有路由
api_router.include_router(devices.router)
api_router.include_router(contents.router)

Binary file not shown.

Binary file not shown.

Binary file not shown.

339
api/contents.py Normal file
View File

@@ -0,0 +1,339 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query, File, UploadFile
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Optional
import json
import os
from database import get_db
from schemas import Content as ContentSchema, ContentCreate, ContentUpdate, ContentResponse
from models import Content as ContentModel, Device as DeviceModel
from mqtt_manager import mqtt_manager
from image_processor import image_processor
from config import settings
router = APIRouter(
prefix="/api",
tags=["contents"]
)
@router.post("/devices/{device_id}/content", response_model=ContentSchema, status_code=status.HTTP_201_CREATED)
async def create_content(
device_id: str,
content: ContentCreate,
db: Session = Depends(get_db)
):
"""
为设备创建新内容版本
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 获取当前最大版本号
max_version = db.query(func.max(ContentModel.version)).filter(
ContentModel.device_id == device_id
).scalar() or 0
# 创建新内容
db_content = ContentModel(
device_id=device_id,
version=max_version + 1,
title=content.title,
description=content.description,
image_path=content.image_path,
layout_config=content.layout_config,
timezone=content.timezone,
time_format=content.time_format
)
db.add(db_content)
db.commit()
db.refresh(db_content)
# 发送MQTT更新通知
mqtt_manager.send_update_command(device_id, db_content.version)
return db_content
@router.get("/devices/{device_id}/content", response_model=List[ContentSchema])
async def list_content(
device_id: str,
skip: int = 0,
limit: int = 100,
is_active: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""
获取设备内容列表
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
query = db.query(ContentModel).filter(ContentModel.device_id == device_id)
if is_active is not None:
query = query.filter(ContentModel.is_active == is_active)
contents = query.order_by(ContentModel.version.desc()).offset(skip).limit(limit).all()
return contents
@router.get("/devices/{device_id}/content/{version}", response_model=ContentResponse)
async def get_content(
device_id: str,
version: int,
db: Session = Depends(get_db)
):
"""
获取特定版本的内容详情
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.version == version
).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="内容不存在"
)
# 构建图片URL
image_url = None
if content.image_path:
# 确保路径是相对路径
rel_path = os.path.relpath(content.image_path)
image_url = f"/static/{rel_path}"
# 解析布局配置
layout_config = None
if content.layout_config:
try:
layout_config = json.loads(content.layout_config)
except json.JSONDecodeError:
layout_config = None
return ContentResponse(
version=content.version,
title=content.title,
description=content.description,
image_url=image_url,
layout_config=layout_config,
timezone=content.timezone,
time_format=content.time_format,
created_at=content.created_at
)
@router.put("/devices/{device_id}/content/{version}", response_model=ContentSchema)
async def update_content(
device_id: str,
version: int,
content_update: ContentUpdate,
db: Session = Depends(get_db)
):
"""
更新内容
"""
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.version == version
).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="内容不存在"
)
# 更新内容信息
update_data = content_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(content, field, value)
db.commit()
db.refresh(content)
# 如果内容被激活发送MQTT更新通知
if content.is_active:
mqtt_manager.send_update_command(device_id, content.version)
return content
@router.delete("/devices/{device_id}/content/{version}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_content(
device_id: str,
version: int,
db: Session = Depends(get_db)
):
"""
删除内容
"""
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.version == version
).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="内容不存在"
)
db.delete(content)
db.commit()
@router.get("/devices/{device_id}/content/latest", response_model=ContentResponse)
async def get_latest_content(device_id: str, db: Session = Depends(get_db)):
"""
获取设备的最新活跃内容
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 获取最新的活跃内容
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.is_active == True
).order_by(ContentModel.version.desc()).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备没有活跃内容"
)
# 构建图片URL
image_url = None
if content.image_path:
# 确保路径是相对路径
rel_path = os.path.relpath(content.image_path)
image_url = f"/static/{rel_path}"
# 解析布局配置
layout_config = None
if content.layout_config:
try:
layout_config = json.loads(content.layout_config)
except json.JSONDecodeError:
layout_config = None
return ContentResponse(
version=content.version,
title=content.title,
description=content.description,
image_url=image_url,
layout_config=layout_config,
timezone=content.timezone,
time_format=content.time_format,
created_at=content.created_at
)
@router.post("/upload")
async def upload_image(
device_id: str = Query(..., description="设备ID"),
version: Optional[int] = Query(None, description="内容版本,如果提供则更新指定版本"),
file: UploadFile = File(...),
db: Session = Depends(get_db)
):
"""
上传图片并处理为墨水屏兼容格式
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 检查文件类型
if not file.content_type.startswith("image/"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="文件必须是图片格式"
)
try:
# 保存上传的文件
file_data = await file.read()
upload_path = image_processor.save_upload(file_data, file.filename)
# 处理图片
processed_path = image_processor.process_image(upload_path)
# 如果提供了版本号,更新指定版本
if version:
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.version == version
).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="指定版本的内容不存在"
)
content.image_path = processed_path
db.commit()
# 发送MQTT更新通知
mqtt_manager.send_update_command(device_id, version)
else:
# 创建新内容版本
max_version = db.query(func.max(ContentModel.version)).filter(
ContentModel.device_id == device_id
).scalar() or 0
content = ContentModel(
device_id=device_id,
version=max_version + 1,
image_path=processed_path,
title=f"图片内容 - {file.filename}",
is_active=True
)
db.add(content)
db.commit()
db.refresh(content)
# 发送MQTT更新通知
mqtt_manager.send_update_command(device_id, content.version)
# 构建图片URL
rel_path = os.path.relpath(processed_path)
image_url = f"/static/{rel_path}"
return {
"message": "图片上传并处理成功",
"image_url": image_url,
"version": content.version if version is None else version
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"图片处理失败: {str(e)}"
)

183
api/devices.py Normal file
View File

@@ -0,0 +1,183 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
import secrets
from database import get_db
from schemas import Device as DeviceSchema, DeviceCreate, DeviceUpdate, BootstrapResponse
from models import Device as DeviceModel
from database import Content as ContentModel
from mqtt_manager import mqtt_manager
router = APIRouter(
prefix="/api/devices",
tags=["devices"]
)
@router.post("/", response_model=DeviceSchema, status_code=status.HTTP_201_CREATED)
async def create_device(device: DeviceCreate, db: Session = Depends(get_db)):
"""
注册新设备
"""
# 检查设备ID是否已存在
db_device = db.query(DeviceModel).filter(DeviceModel.device_id == device.device_id).first()
if db_device:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="设备ID已存在"
)
# 创建新设备
secret = device.secret if device.secret else secrets.token_urlsafe(32)
db_device = DeviceModel(
device_id=device.device_id,
secret=secret,
name=device.name,
scene=device.scene
)
db.add(db_device)
db.commit()
db.refresh(db_device)
# 订阅设备状态
mqtt_manager.subscribe_to_device_status(device.device_id)
return db_device
@router.get("/", response_model=List[DeviceSchema])
async def list_devices(
skip: int = 0,
limit: int = 100,
scene: Optional[str] = None,
is_active: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""
获取设备列表
"""
query = db.query(DeviceModel)
if scene:
query = query.filter(DeviceModel.scene == scene)
if is_active is not None:
query = query.filter(DeviceModel.is_active == is_active)
devices = query.offset(skip).limit(limit).all()
return devices
@router.get("/{device_id}", response_model=DeviceSchema)
async def get_device(device_id: str, db: Session = Depends(get_db)):
"""
获取设备详情
"""
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
return device
@router.put("/{device_id}", response_model=DeviceSchema)
async def update_device(
device_id: str,
device_update: DeviceUpdate,
db: Session = Depends(get_db)
):
"""
更新设备信息
"""
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 更新设备信息
update_data = device_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(device, field, value)
device.updated_at = datetime.utcnow()
db.commit()
db.refresh(device)
return device
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_device(device_id: str, db: Session = Depends(get_db)):
"""
删除设备
"""
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 取消订阅设备状态
mqtt_manager.unsubscribe_from_device_status(device_id)
db.delete(device)
db.commit()
@router.get("/{device_id}/bootstrap", response_model=BootstrapResponse)
async def device_bootstrap(device_id: str, db: Session = Depends(get_db)):
"""
设备启动获取当前版本信息
"""
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 更新设备最后在线时间
device.last_online = datetime.utcnow()
db.commit()
# 获取最新的活跃内容
latest_content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.is_active == True
).order_by(ContentModel.version.desc()).first()
response = BootstrapResponse(
device_id=device_id,
timezone=latest_content.timezone if latest_content else "Asia/Shanghai",
time_format=latest_content.time_format if latest_content else "%Y-%m-%d %H:%M"
)
if latest_content:
response.content_version = latest_content.version
response.last_updated = latest_content.created_at
return response
@router.get("/{device_id}/status")
async def get_device_status(device_id: str, db: Session = Depends(get_db)):
"""
获取设备状态
"""
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
return {
"device_id": device_id,
"name": device.name,
"scene": device.scene,
"is_active": device.is_active,
"last_online": device.last_online,
"created_at": device.created_at,
"updated_at": device.updated_at
}