feat(points): 实现积分流水列表功能

- 后端新增 GET /api/v1/points/ledger 接口
- 前端新增积分流水列表页面
- 积分中心添加「查看流水」入口
- 重命名 AccountDeleteScreen 为 AccountDataScreen
- 流水列表支持分页加载和空状态展示
This commit is contained in:
ZL-Q
2026-04-28 17:19:08 +08:00
parent a83001de0d
commit 940c67e642
12 changed files with 794 additions and 70 deletions
+55 -2
View File
@@ -1,18 +1,42 @@
from __future__ import annotations
from datetime import datetime
from typing import Annotated
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from core.auth.models import CurrentUser
from core.http.errors import ApiProblemError, problem_payload
from v1.points.dependencies import get_points_service
from v1.points.schemas import PackagesResponse, PackageInfo, PointsBalanceResponse
from v1.points.schemas import (
PackagesResponse,
PackageInfo,
PointsBalanceResponse,
LedgerListResponse,
LedgerItem,
)
from v1.points.service import PointsService
from v1.users.dependencies import get_current_user
router = APIRouter(prefix="/points", tags=["points"])
def _parse_cursor(cursor: str | None) -> datetime | None:
if cursor is None:
return None
try:
return datetime.fromisoformat(cursor.replace("Z", "+00:00"))
except ValueError as exc:
raise ApiProblemError(
status_code=422,
detail=problem_payload(
code="POINTS_INVALID_CURSOR",
detail="Points ledger cursor must be an ISO 8601 datetime",
params={"cursor": cursor},
),
) from exc
@router.get("/balance", response_model=PointsBalanceResponse)
async def get_points_balance(
service: Annotated[PointsService, Depends(get_points_service)],
@@ -55,3 +79,32 @@ async def get_available_packages(
for pkg in result.packages
],
)
@router.get("/ledger", response_model=LedgerListResponse)
async def get_points_ledger(
service: Annotated[PointsService, Depends(get_points_service)],
current_user: Annotated[CurrentUser, Depends(get_current_user)],
limit: Annotated[int, Query(ge=1, le=100)] = 20,
cursor: str | None = None,
) -> LedgerListResponse:
items, next_cursor, has_more = await service.get_ledger_list(
user_id=current_user.id,
limit=limit,
cursor=_parse_cursor(cursor),
)
return LedgerListResponse(
items=[
LedgerItem(
id=item.id,
direction=item.direction,
amount=item.amount,
balanceAfter=item.balance_after,
changeType=item.change_type,
createdAt=item.created_at,
)
for item in items
],
nextCursor=next_cursor,
hasMore=has_more,
)