Files
banban/talkingq-url/banban/routers/bindings.py
2026-04-30 05:11:58 +08:00

281 lines
8.0 KiB
Python

import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel
from banban.dao.binding import (
SESSION_STATUS_CANCELLED,
SESSION_STATUS_COMPLETED,
SESSION_STATUS_EXPIRED,
SESSION_STATUS_FAILED,
SESSION_STATUS_PENDING,
)
from banban.security import get_current_user_id
from banban.service.binding import BindingError, BindingService
router = APIRouter(prefix="/bindings", tags=["bindings"])
logger = logging.getLogger("app.bindings")
class BindStartRequest(BaseModel):
device_id: str
serial_number: str
child_id: int | None = None
class BindStartResponse(BaseModel):
bind_token: str
expires_at: str
status: int
class BindSessionResponse(BaseModel):
bind_token: str
device_id: str
child_id: int | None = None
status: int
expires_at: str
card_uuid: str | None = None
class BindConfirmRequest(BaseModel):
bind_token: str
challenge_code: str
class BindConfirmResponse(BaseModel):
device_id: str
child_id: int | None
class BindingGetResponse(BaseModel):
device_id: str
child_id: int | None
status: int
bound_at: datetime
class BindingListItem(BaseModel):
device_id: str
child_id: int | None
child_name: str | None = None
status: int
bound_at: datetime
class BindingListResponse(BaseModel):
items: list[BindingListItem]
total: int
next_cursor: int | None = None
class BindHistoryItem(BaseModel):
device_id: str
child_id: int | None
bound_at: datetime
unbound_at: datetime | None
class BindHistoryResponse(BaseModel):
items: list[BindHistoryItem]
total: int
next_cursor: str | None
@router.post("/start", response_model=BindStartResponse)
async def start_bind(
payload: BindStartRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> BindStartResponse:
del request
service = BindingService()
try:
bind_token, expires_at = await service.start_bind(
current_user_id,
payload.device_id,
payload.serial_number,
payload.child_id,
)
except BindingError as exc:
raise HTTPException(status_code=exc.status_code, detail=str(exc))
return BindStartResponse(
bind_token=bind_token,
expires_at=expires_at.isoformat(),
status=SESSION_STATUS_PENDING,
)
@router.get("/sessions/{bind_token}", response_model=BindSessionResponse)
async def get_bind_session(
bind_token: str,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> BindSessionResponse:
del request
service = BindingService()
session = await service.get_bind_session(bind_token, current_user_id)
if not session:
raise HTTPException(status_code=404, detail="bind session not found")
return BindSessionResponse(
bind_token=session["bind_token"],
device_id=session["device_id"],
child_id=session["target_child_id"],
status=int(session["status"]),
expires_at=session["expires_at"].isoformat(),
card_uuid=session.get("card_uuid"),
)
@router.post("/confirm", response_model=BindConfirmResponse)
async def confirm_bind(
payload: BindConfirmRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> BindConfirmResponse:
del request
service = BindingService()
try:
result = await service.confirm_bind(payload.bind_token, current_user_id)
except BindingError as exc:
raise HTTPException(status_code=exc.status_code, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return BindConfirmResponse(**result)
class DirectBindRequest(BaseModel):
device_id: str
serial_number: str
child_id: int | None = None
class DirectBindResponse(BaseModel):
device_id: str
child_id: int | None
class BindSetChildRequest(BaseModel):
child_id: int
@router.post("/direct", response_model=DirectBindResponse)
async def direct_bind(
payload: DirectBindRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> DirectBindResponse:
del request
service = BindingService()
try:
result = await service.direct_bind(
payload.device_id,
payload.serial_number,
payload.child_id,
current_user_id,
)
except BindingError as exc:
raise HTTPException(status_code=exc.status_code, detail=str(exc))
return DirectBindResponse(**result)
@router.patch("/{device_id}/child", response_model=DirectBindResponse)
async def set_binding_child(
device_id: str,
payload: BindSetChildRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> DirectBindResponse:
del request
service = BindingService()
try:
result = await service.set_binding_child(device_id=device_id, child_id=payload.child_id, user_id=current_user_id)
except BindingError as exc:
raise HTTPException(status_code=exc.status_code, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc))
return DirectBindResponse(**result)
@router.get("/current", response_model=BindingGetResponse)
async def get_current_binding(
request: Request,
current_user_id: int = Depends(get_current_user_id),
):
del request
service = BindingService()
binding = await service.get_current_binding(current_user_id)
if not binding:
raise HTTPException(status_code=404, detail="no binding found")
return binding
@router.get("", response_model=BindingListResponse)
async def list_bindings(
request: Request,
cursor: int | None = Query(default=None, ge=1),
limit: int = Query(default=20, ge=1, le=100),
current_user_id: int = Depends(get_current_user_id),
) -> BindingListResponse:
del request
service = BindingService()
rows, has_more = await service.list_bindings(current_user_id, limit, cursor)
next_cursor = int(rows[-1]["id"]) if has_more and rows else None
items = [
BindingListItem(
device_id=row["device_id"],
child_id=row["child_id"],
child_name=row["child_name"],
status=row["status"],
bound_at=row["bound_at"],
)
for row in rows
]
return BindingListResponse(items=items, total=len(items), next_cursor=next_cursor)
@router.get("/{device_id}", response_model=BindingGetResponse)
async def get_binding(
device_id: str,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> BindingGetResponse:
del request
service = BindingService()
binding = await service.get_binding(device_id, current_user_id)
if not binding:
raise HTTPException(status_code=404, detail="binding not found")
return BindingGetResponse(**binding)
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
async def unbind_device(
device_id: str,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> None:
del request
service = BindingService()
if not await service.unbind(device_id, current_user_id):
raise HTTPException(status_code=404, detail="binding not found")
@router.get("/history/{device_id}", response_model=BindHistoryResponse)
async def get_bind_history(
device_id: str,
request: Request,
cursor: str | None = None,
limit: int = 20,
current_user_id: int = Depends(get_current_user_id),
) -> BindHistoryResponse:
del request, current_user_id
cursor_dt = datetime.fromisoformat(cursor) if cursor else None
service = BindingService()
rows, has_more = await service.list_history(device_id, limit, cursor_dt)
next_cursor = rows[-1]["bound_at"].isoformat() if has_more and rows else None
items = [BindHistoryItem(**row) for row in rows]
return BindHistoryResponse(items=items, total=len(items), next_cursor=next_cursor)