feat: integrate invite API and improve notification handling

- Add invite code display and binding functionality via API
- Fix notification unread count sync on auth state change
- Improve notification mark read with server state validation
- Add auth state listener to trigger notification refresh
- Add YaoCoinConverter for coin-to-yao type conversion
- Remove YaoLegend from divination screens (UI cleanup)
- Abbreviate relation labels in yao detail view
- Add re-register notice to account delete screen
- Update 'coins' terminology to 'points' in localization
- Fix backend points consumption to only run in CHAT mode
- Add HttpxAuthNoiseFilter to suppress auth endpoint logging
- Fix notification static_schema import path
- Add test coverage for notification bloc error handling
- Update AGENTS.md page header rules and image handling
- Delete deprecated run-dev.sh script
This commit is contained in:
qzl
2026-04-13 14:52:22 +08:00
parent da947f9f08
commit 1e22f27de2
52 changed files with 1419 additions and 307 deletions
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth.models import CurrentUser
from core.db import get_db
from v1.invite.repository import InviteCodeRepository
from v1.invite.service import InviteCodeService
from v1.users.dependencies import get_current_user
def get_invite_code_repository(
session: Annotated[AsyncSession, Depends(get_db)],
) -> InviteCodeRepository:
return InviteCodeRepository(session)
def get_invite_code_service(
repository: Annotated[InviteCodeRepository, Depends(get_invite_code_repository)],
) -> InviteCodeService:
return InviteCodeService(repository=repository)
def get_current_user_for_invite(
current_user: Annotated[CurrentUser, Depends(get_current_user)],
) -> CurrentUser:
return current_user
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
from uuid import UUID
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models.invite_code import InviteCode
class InviteCodeRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_owner_id(self, *, owner_id: UUID) -> InviteCode | None:
stmt = (
select(InviteCode)
.where(InviteCode.owner_id == owner_id)
.order_by(InviteCode.created_at.desc())
.limit(1)
)
return (await self._session.execute(stmt)).scalar_one_or_none()
+24
View File
@@ -0,0 +1,24 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends
from core.auth.models import CurrentUser
from v1.invite.dependencies import (
get_current_user_for_invite,
get_invite_code_service,
)
from v1.invite.schemas import MyInviteCodeResponse
from v1.invite.service import InviteCodeService
router = APIRouter(prefix="/invite", tags=["invite"])
@router.get("/me", response_model=MyInviteCodeResponse)
async def get_my_invite_code(
current_user: Annotated[CurrentUser, Depends(get_current_user_for_invite)],
service: Annotated[InviteCodeService, Depends(get_invite_code_service)],
) -> MyInviteCodeResponse:
return await service.get_my_invite_code(user_id=current_user.id)
+10
View File
@@ -0,0 +1,10 @@
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
class MyInviteCodeResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
code: str
used_count: int
+28
View File
@@ -0,0 +1,28 @@
from __future__ import annotations
from dataclasses import dataclass
from uuid import UUID
from core.http.errors import ApiProblemError, problem_payload
from v1.invite.repository import InviteCodeRepository
from v1.invite.schemas import MyInviteCodeResponse
@dataclass
class InviteCodeService:
repository: InviteCodeRepository
async def get_my_invite_code(self, *, user_id: UUID) -> MyInviteCodeResponse:
invite_code = await self.repository.get_by_owner_id(owner_id=user_id)
if invite_code is None:
raise ApiProblemError(
status_code=404,
detail=problem_payload(
code="INVITE_CODE_NOT_FOUND",
detail="Invite code not found for current user",
),
)
return MyInviteCodeResponse(
code=invite_code.code,
used_count=invite_code.used_count,
)