支持绑定卡片管理和名称编辑
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user