支持家庭成员显示名编辑

This commit is contained in:
stu2not
2026-06-08 11:46:14 +08:00
parent dcff337f43
commit ac9d669107
10 changed files with 255 additions and 6 deletions

View File

@@ -286,10 +286,12 @@ class FamilyDAO(BaseDAO):
dfm.device_id,
dfm.user_id,
dfm.role,
dfm.display_name,
dfm.status,
dfm.joined_at,
dfm.invited_by_user_id,
p.nickname,
p.nickname AS account_nickname,
COALESCE(NULLIF(dfm.display_name, ''), p.nickname) AS nickname,
p.avatar_url,
CASE WHEN db.owner_user_id = dfm.user_id THEN 1 ELSE 0 END AS is_owner
FROM device_family_members AS dfm
@@ -311,6 +313,45 @@ class FamilyDAO(BaseDAO):
).mappings().all()
return list(rows)
async def update_member_display_name(
self,
*,
device_id: str,
member_user_id: int,
owner_user_id: int,
display_name: str | None,
) -> Mapping | None:
owner_binding = await self.get_binding_for_owner(device_id=device_id, user_id=owner_user_id)
if owner_binding is None:
return None
result = await self.execute(
text(
"""
UPDATE device_family_members
SET display_name = :display_name,
updated_at = CURRENT_TIMESTAMP
WHERE device_id = :device_id
AND user_id = :member_user_id
AND status = :active_status
"""
),
{
"device_id": device_id,
"member_user_id": member_user_id,
"display_name": display_name,
"active_status": FAMILY_STATUS_ACTIVE,
},
)
if not result.rowcount:
return None
rows = await self.list_members(device_id=device_id, user_id=owner_user_id)
for row in rows:
if int(row["user_id"]) == member_user_id:
return row
return None
async def list_active_member_user_ids(self, *, device_id: str) -> list[int]:
await self.ensure_owner_member(device_id=device_id)
rows = (

View File

@@ -13,12 +13,18 @@ router = APIRouter(prefix="/family", tags=["family"])
class FamilyMemberItem(BaseModel):
user_id: int
nickname: str | None = None
display_name: str | None = None
account_nickname: str | None = None
avatar_url: str | None = None
role: str
is_owner: bool
joined_at: datetime | None = None
class FamilyMemberUpdateRequest(BaseModel):
display_name: str | None = None
class FamilyMemberListResponse(BaseModel):
device_id: str
max_members: int
@@ -77,6 +83,8 @@ async def list_family_members(
FamilyMemberItem(
user_id=int(row["user_id"]),
nickname=row.get("nickname"),
display_name=row.get("display_name"),
account_nickname=row.get("account_nickname"),
avatar_url=row.get("avatar_url"),
role=role,
is_owner=is_owner,
@@ -133,6 +141,34 @@ async def accept_family_invitation(
return FamilyInvitationAcceptResponse(**result)
@router.patch("/devices/{device_id}/members/{member_user_id}", response_model=FamilyMemberItem)
async def update_family_member(
device_id: str,
member_user_id: int,
payload: FamilyMemberUpdateRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> FamilyMemberItem:
del request
row = await family_service.update_member_display_name(
device_id=device_id,
member_user_id=member_user_id,
display_name=payload.display_name,
user_id=current_user_id,
)
is_owner = bool(row["is_owner"])
return FamilyMemberItem(
user_id=int(row["user_id"]),
nickname=row.get("nickname"),
display_name=row.get("display_name"),
account_nickname=row.get("account_nickname"),
avatar_url=row.get("avatar_url"),
role=_role_name(row["role"], is_owner=is_owner),
is_owner=is_owner,
joined_at=row.get("joined_at"),
)
@router.delete("/devices/{device_id}/members/{member_user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_family_member(
device_id: str,

View File

@@ -16,6 +16,14 @@ class FamilyService(DatabaseServiceBase):
def __init__(self):
super().__init__(service_name="family_service")
def _normalize_display_name(self, display_name: str | None) -> str | None:
normalized = str(display_name or "").strip()
if not normalized:
return None
if len(normalized) > 64:
raise HTTPException(status_code=400, detail="display name must be 64 characters or fewer")
return normalized
async def ensure_device_access(self, *, device_id: str, user_id: int) -> Mapping:
db_session = await self.get_session()
try:
@@ -184,6 +192,39 @@ class FamilyService(DatabaseServiceBase):
finally:
await db_session.close()
async def update_member_display_name(
self,
*,
device_id: str,
member_user_id: int,
display_name: str | None,
user_id: int,
) -> Mapping:
normalized_display_name = self._normalize_display_name(display_name)
db_session = await self.get_session()
try:
dao = FamilyDAO(db_session)
owner_binding = await dao.get_binding_for_owner(device_id=device_id, user_id=user_id)
if owner_binding is None:
raise HTTPException(status_code=403, detail="only owner can edit family member display names")
member = await dao.update_member_display_name(
device_id=device_id,
member_user_id=member_user_id,
owner_user_id=user_id,
display_name=normalized_display_name,
)
if member is None:
raise HTTPException(status_code=404, detail="family member not found")
await db_session.commit()
return member
except Exception:
await db_session.rollback()
raise
finally:
await db_session.close()
async def leave_family(self, *, device_id: str, user_id: int) -> bool:
db_session = await self.get_session()
try:

View File

@@ -170,6 +170,7 @@ async def _ensure_device_family_tables(conn) -> None:
device_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
role TINYINT NOT NULL DEFAULT 2,
display_name VARCHAR(64) NULL,
status TINYINT NOT NULL DEFAULT 1,
invited_by_user_id BIGINT NULL,
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -195,6 +196,13 @@ async def _ensure_device_family_tables(conn) -> None:
"""
)
)
await _ensure_columns(
conn,
table_name="device_family_members",
columns=[
("display_name", "display_name VARCHAR(64) NULL AFTER role"),
],
)
await conn.execute(
text(
"""

View File

@@ -242,6 +242,7 @@ class DeviceFamilyMember(Base):
device_id: Mapped[str] = mapped_column(String(64), ForeignKey("device_auth.device_id"), nullable=False)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("parents.user_id"), nullable=False)
role: Mapped[int] = mapped_column(Integer, server_default="2")
display_name: Mapped[Optional[str]] = mapped_column(String(64))
status: Mapped[int] = mapped_column(Integer, server_default="1")
invited_by_user_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("parents.user_id", ondelete="SET NULL"))
joined_at: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))

View File

@@ -277,6 +277,7 @@ CREATE TABLE IF NOT EXISTS `device_family_members` (
`device_id` VARCHAR(64) NOT NULL,
`user_id` BIGINT NOT NULL,
`role` TINYINT NOT NULL DEFAULT 2 COMMENT '1=owner,2=member',
`display_name` VARCHAR(64) NULL,
`status` TINYINT NOT NULL DEFAULT 1,
`invited_by_user_id` BIGINT NULL,
`joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,