支持绑定卡片管理和名称编辑

This commit is contained in:
stu2not
2026-06-08 10:48:30 +08:00
parent d33212613b
commit dcff337f43
6 changed files with 541 additions and 1 deletions

View File

@@ -464,6 +464,85 @@ class BindingDAO(BaseDAO):
).mappings().all()
return rows
async def list_card_groups_by_user(self, user_id: int) -> list[Mapping]:
return (
await self.execute(
"""
SELECT
db.device_id,
db.child_id,
c.child_name,
db.bound_at,
card.card_id,
card.card_uuid,
card.card_name,
card.status AS card_status,
card.updated_at AS card_updated_at
FROM device_bindings AS db
LEFT JOIN children AS c
ON c.child_id = db.child_id
AND c.status = 1
LEFT JOIN device_family_members AS dfm
ON dfm.device_id = db.device_id
AND dfm.user_id = :user_id
AND dfm.status = 1
LEFT JOIN cards AS card
ON card.device_id = db.device_id
AND card.status = 1
WHERE db.status = 1
AND (
db.owner_user_id = :user_id
OR dfm.id IS NOT NULL
)
ORDER BY db.bound_at DESC, db.device_id ASC, card.updated_at DESC, card.card_id DESC
""",
{"user_id": user_id},
)
).mappings().all()
async def get_visible_card_by_id(self, card_id: int, user_id: int) -> Optional[Mapping]:
return (
await self.execute(
"""
SELECT
card.card_id,
card.card_uuid,
card.card_name,
card.status AS card_status,
card.updated_at AS card_updated_at,
db.device_id,
db.owner_user_id
FROM cards AS card
JOIN device_bindings AS db
ON db.device_id = card.device_id
AND db.status = 1
LEFT JOIN device_family_members AS dfm
ON dfm.device_id = db.device_id
AND dfm.user_id = :user_id
AND dfm.status = 1
WHERE card.card_id = :card_id
AND card.status = 1
AND (
db.owner_user_id = :user_id
OR dfm.id IS NOT NULL
)
LIMIT 1
""",
{"card_id": card_id, "user_id": user_id},
)
).mappings().first()
async def update_card_name(self, card_id: int, card_name: Optional[str]) -> None:
await self.execute(
"""
UPDATE cards
SET card_name = :card_name,
updated_at = CURRENT_TIMESTAMP
WHERE card_id = :card_id
""",
{"card_id": card_id, "card_name": card_name},
)
async def get_by_device(self, device_id: str, user_id: int) -> Optional[Mapping]:
return (
await self.execute(

View File

@@ -75,6 +75,33 @@ class BindingListResponse(BaseModel):
next_cursor: int | None = None
class BindingCardItem(BaseModel):
card_id: int
card_uuid: str
card_name: str | None = None
status: int
updated_at: datetime | None = None
class BindingCardUpdateRequest(BaseModel):
card_name: str | None = None
class BindingCardGroupItem(BaseModel):
device_id: str
child_id: int | None = None
child_name: str | None = None
bound_at: datetime
card_count: int
cards: list[BindingCardItem]
class BindingCardGroupListResponse(BaseModel):
items: list[BindingCardGroupItem]
total_devices: int
total_cards: int
class BindHistoryItem(BaseModel):
device_id: str
child_id: int | None
@@ -252,6 +279,50 @@ async def list_bindings(
return BindingListResponse(items=items, total=len(items), next_cursor=next_cursor)
@router.get("/cards", response_model=BindingCardGroupListResponse)
async def list_binding_cards(
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> BindingCardGroupListResponse:
del request
groups = await binding_service.list_card_groups(current_user_id)
items = [
BindingCardGroupItem(
device_id=group["device_id"],
child_id=group["child_id"],
child_name=group["child_name"],
bound_at=group["bound_at"],
card_count=len(group["cards"]),
cards=[BindingCardItem(**card) for card in group["cards"]],
)
for group in groups
]
return BindingCardGroupListResponse(
items=items,
total_devices=len(items),
total_cards=sum(item.card_count for item in items),
)
@router.patch("/cards/{card_id}", response_model=BindingCardItem)
async def update_binding_card(
card_id: int,
payload: BindingCardUpdateRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> BindingCardItem:
del request
try:
card = await binding_service.update_card_name(
card_id=card_id,
card_name=payload.card_name,
user_id=current_user_id,
)
except BindingError as exc:
raise HTTPException(status_code=exc.status_code, detail=str(exc))
return BindingCardItem(**card)
@router.get("/{device_id}", response_model=BindingGetResponse)
async def get_binding(
device_id: str,

View File

@@ -1,6 +1,6 @@
from collections.abc import Mapping
from datetime import datetime
from typing import Optional
from typing import Any, Optional
from banban.dao.binding import (
BIND_MODE_CARD,
@@ -26,6 +26,14 @@ class BindingService(DatabaseServiceBase):
def __init__(self):
super().__init__(service_name="binding_service")
def _normalize_card_name(self, card_name: str | None) -> str | None:
normalized = str(card_name or "").strip()
if not normalized:
return None
if len(normalized) > 64:
raise BindingError("card name must be 64 characters or fewer", status_code=400)
return normalized
async def _ensure_device_unbound(self, db_session, device_id: str) -> None:
dao = BindingDAO(db_session)
active_binding = await dao.get_active_binding_by_device(device_id)
@@ -221,6 +229,67 @@ class BindingService(DatabaseServiceBase):
finally:
await db_session.close()
async def list_card_groups(self, user_id: int) -> list[Mapping]:
db_session = await self.get_session()
try:
dao = BindingDAO(db_session)
rows = await dao.list_card_groups_by_user(user_id)
groups: dict[str, dict[str, Any]] = {}
for row in rows:
device_id = str(row["device_id"])
group = groups.get(device_id)
if group is None:
group = {
"device_id": device_id,
"child_id": row["child_id"],
"child_name": row["child_name"],
"bound_at": row["bound_at"],
"cards": [],
}
groups[device_id] = group
if row["card_id"] is None:
continue
group["cards"].append(
{
"card_id": int(row["card_id"]),
"card_uuid": row["card_uuid"],
"card_name": row["card_name"],
"status": int(row["card_status"]),
"updated_at": row["card_updated_at"],
}
)
return list(groups.values())
finally:
await db_session.close()
async def update_card_name(self, *, card_id: int, card_name: str | None, user_id: int) -> Mapping:
normalized_name = self._normalize_card_name(card_name)
db_session = await self.get_session()
try:
dao = BindingDAO(db_session)
card = await dao.get_visible_card_by_id(card_id=card_id, user_id=user_id)
if card is None:
raise BindingError("card not found", status_code=404)
if int(card["owner_user_id"]) != user_id:
raise BindingError("only the device owner can rename cards", status_code=403)
await dao.update_card_name(card_id=card_id, card_name=normalized_name)
updated_card = await dao.get_visible_card_by_id(card_id=card_id, user_id=user_id)
await db_session.commit()
card = updated_card or card
return {
"card_id": int(card["card_id"]),
"card_uuid": card["card_uuid"],
"card_name": normalized_name,
"status": int(card["card_status"]),
"updated_at": card["card_updated_at"],
}
finally:
await db_session.close()
async def direct_bind(
self,
device_id: str,