From d71b50c20009dc08417c9773ac0b94367283fa1b Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 17:50:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-api/app/api/v1/credits.py | 16 ++++-- video-gen-api/app/api/v1/notifications.py | 9 +-- video-gen-api/app/api/v1/payments.py | 13 ++++- video-gen-api/app/schemas/notification.py | 5 ++ video-gen-api/app/services/credits.py | 9 ++- video-gen-api/app/services/notification.py | 24 ++++++-- video-gen-app/src/api/index.ts | 34 +++++++++--- video-gen-app/src/api/mock.ts | 3 +- .../src/components/NotificationPopup.tsx | 3 +- video-gen-app/src/pages/CreditRecordsPage.tsx | 39 +++++++++---- video-gen-app/src/pages/MessagesPage.tsx | 55 ++++++++++++++----- video-gen-app/src/pages/OrderRecordsPage.tsx | 41 ++++++++++---- 12 files changed, 187 insertions(+), 64 deletions(-) diff --git a/video-gen-api/app/api/v1/credits.py b/video-gen-api/app/api/v1/credits.py index 243fceb1..3948753d 100644 --- a/video-gen-api/app/api/v1/credits.py +++ b/video-gen-api/app/api/v1/credits.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select @@ -17,14 +17,18 @@ router = APIRouter(prefix="/credits", tags=["credits"]) @router.get("", response_model=CreditBalanceOut) async def get_credits( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - records = await get_records(db, current_user.id) - return CreditBalanceOut( - credits=round(current_user.credits, 2), - records=[CreditRecordOut.model_validate(r) for r in records], - ) + records, total = await get_records(db, current_user.id, page, page_size) + from fastapi.responses import JSONResponse + return JSONResponse(content={ + "credits": round(current_user.credits, 2), + "records": [CreditRecordOut.model_validate(r) for r in records], + "total": total, + }) @router.get( diff --git a/video-gen-api/app/api/v1/notifications.py b/video-gen-api/app/api/v1/notifications.py index 7a5ed5e3..9f52748f 100644 --- a/video-gen-api/app/api/v1/notifications.py +++ b/video-gen-api/app/api/v1/notifications.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db, get_current_user from app.models.user import User from app.models.notification import Notification -from app.schemas.notification import NotificationOut, UnreadCountOut +from app.schemas.notification import NotificationOut, NotificationListOut, UnreadCountOut from app.services.auth import decode_access_token from app.services.notification import ( get_notifications, @@ -103,15 +103,16 @@ async def notifications_ws(websocket: WebSocket): _active_connections.pop(user_id, None) -@router.get("", response_model=list[NotificationOut]) +@router.get("", response_model=NotificationListOut) async def list_notifications( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), + is_read: bool = Query(None), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - items, _ = await get_notifications(db, current_user.id, page, page_size) - return items + items, total = await get_notifications(db, current_user.id, page, page_size, is_read) + return {"items": items, "total": total} @router.put("/{notification_id}/read") diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 6fbda84b..69498666 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -290,20 +290,29 @@ async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)): @router.get("/orders", response_model=list[PaymentOrderOut]) async def list_orders( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - # Auto-expire stale pending orders before returning from app.services.payment import _check_and_expire_order + + count_query = select(func.count(PaymentOrder.id)).where(PaymentOrder.user_id == current_user.id) + total = (await db.execute(count_query)).scalar() or 0 + result = await db.execute( select(PaymentOrder) .where(PaymentOrder.user_id == current_user.id) .order_by(PaymentOrder.created_at.desc()) + .offset((page - 1) * page_size) + .limit(page_size) ) orders = result.scalars().all() for o in orders: await _check_and_expire_order(db, o) - return orders + + from fastapi.responses import JSONResponse + return JSONResponse(content={"items": orders, "total": total}) @router.get("/orders/{order_no}", response_model=PaymentOrderOut) diff --git a/video-gen-api/app/schemas/notification.py b/video-gen-api/app/schemas/notification.py index 26d2b990..93d2f272 100644 --- a/video-gen-api/app/schemas/notification.py +++ b/video-gen-api/app/schemas/notification.py @@ -14,5 +14,10 @@ class NotificationOut(BaseModel): model_config = {"from_attributes": True} +class NotificationListOut(BaseModel): + items: list[NotificationOut] + total: int + + class UnreadCountOut(BaseModel): count: int diff --git a/video-gen-api/app/services/credits.py b/video-gen-api/app/services/credits.py index ab4de901..47a8d00c 100644 --- a/video-gen-api/app/services/credits.py +++ b/video-gen-api/app/services/credits.py @@ -282,10 +282,15 @@ async def refund_credits( ) -async def get_records(db: AsyncSession, user_id: str) -> list[CreditRecord]: +async def get_records(db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20) -> tuple[list[CreditRecord], int]: + count_query = select(func.count(CreditRecord.id)).where(CreditRecord.user_id == user_id) + total = (await db.execute(count_query)).scalar() or 0 + result = await db.execute( select(CreditRecord) .where(CreditRecord.user_id == user_id) .order_by(CreditRecord.created_at.desc()) + .offset((page - 1) * page_size) + .limit(page_size) ) - return list(result.scalars().all()) + return list(result.scalars().all()), total diff --git a/video-gen-api/app/services/notification.py b/video-gen-api/app/services/notification.py index 7dfb16d4..36c82cee 100644 --- a/video-gen-api/app/services/notification.py +++ b/video-gen-api/app/services/notification.py @@ -74,16 +74,18 @@ async def create_notification( async def get_notifications( - db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20 + db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20, is_read: bool = None ) -> tuple[list[dict], int]: """Get notifications for a user (personal + broadcast), with per-user read state.""" + user_is_read_expr = case( + (Notification.user_id.is_(None), NotificationRead.id.is_not(None)), + else_=Notification.is_read, + ).label("user_is_read") + query = ( select( Notification, - case( - (Notification.user_id.is_(None), NotificationRead.id.is_not(None)), - else_=Notification.is_read, - ).label("user_is_read"), + user_is_read_expr, ) .outerjoin( NotificationRead, @@ -96,11 +98,15 @@ async def get_notifications( Notification.user_id.is_(None), ) ) - .order_by(Notification.created_at.desc()) ) count_query = ( select(func.count(Notification.id)) + .outerjoin( + NotificationRead, + (Notification.id == NotificationRead.notification_id) + & (NotificationRead.user_id == user_id), + ) .where( or_( Notification.user_id == user_id, @@ -109,6 +115,12 @@ async def get_notifications( ) ) + if is_read is not None: + query = query.where(user_is_read_expr == is_read) + count_query = count_query.where(user_is_read_expr == is_read) + + query = query.order_by(Notification.created_at.desc()) + total = (await db.execute(count_query)).scalar() or 0 result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) rows = result.all() diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 4c52de8c..1daeffe8 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -153,9 +153,15 @@ export async function generateVideo(recordId: string, params: GenerateParams): P }); } // ── Credits ─────────────────────────────────────────────── -export async function getCredits(): Promise<{ credits: number; records: CreditRecord[] }> { - if (USE_MOCK) return mock.mockGetCredits(); - return api.get('/credits'); +export async function getCredits(page = 1, pageSize = 20): Promise<{ credits: number; records: CreditRecord[]; total: number }> { + if (USE_MOCK) { + const data = await mock.mockGetCredits(); + return data; + } + const params = new URLSearchParams(); + params.set('page', String(page)); + params.set('page_size', String(pageSize)); + return api.get(`/credits?${params.toString()}`); } // ── Captcha ─────────────────────────────────────────────── export async function getSliderCaptcha(): Promise<{ captcha_id: string; bg_image: string; slider_image: string }> { @@ -191,9 +197,18 @@ export async function verifySms(phone: string, code: string): Promise<{ token: s return api.post('/sms/verify', { phone, code }, false); } // ── Notifications ───────────────────────────────────────── -export async function getNotifications(): Promise { - if (USE_MOCK) return mock.mockGetAdminNotifications(); - return api.get('/notifications'); +export async function getNotifications(page = 1, pageSize = 20, isRead?: boolean): Promise<{ items: AdminNotification[], total: number }> { + if (USE_MOCK) { + const items = await mock.mockGetAdminNotifications(); + return { items, total: items.length }; + } + const params = new URLSearchParams(); + params.set('page', String(page)); + params.set('page_size', String(pageSize)); + if (isRead !== undefined) { + params.set('is_read', String(isRead)); + } + return api.get(`/notifications?${params.toString()}`); } export async function getUnreadCount(): Promise { if (USE_MOCK) return mock.mockGetAdminNotifications().then(n => n.filter(x => !x.isRead).length); @@ -271,8 +286,11 @@ export async function createRechargeOrder(planId: string, method: string = 'wech return api.post('/payments/recharge', { plan: planId, method }); } -export async function getPaymentOrders(): Promise { - return api.get('/payments/orders'); +export async function getPaymentOrders(page = 1, pageSize = 20): Promise<{ items: any[]; total: number }> { + const params = new URLSearchParams(); + params.set('page', String(page)); + params.set('page_size', String(pageSize)); + return api.get(`/payments/orders?${params.toString()}`); } export async function getPaymentOrder(orderNo: string): Promise { diff --git a/video-gen-app/src/api/mock.ts b/video-gen-app/src/api/mock.ts index 75af7a0f..333dbcf5 100644 --- a/video-gen-app/src/api/mock.ts +++ b/video-gen-app/src/api/mock.ts @@ -157,11 +157,12 @@ export async function mockGetUser(): Promise { return currentUser; } -export async function mockGetCredits(): Promise<{ credits: number; records: CreditRecord[] }> { +export async function mockGetCredits(): Promise<{ credits: number; records: CreditRecord[]; total: number }> { await delay(400); return { credits: currentUser?.credits ?? 0, records: MOCK_CREDIT_RECORDS, + total: MOCK_CREDIT_RECORDS.length, }; } diff --git a/video-gen-app/src/components/NotificationPopup.tsx b/video-gen-app/src/components/NotificationPopup.tsx index 742059de..2a29d15a 100644 --- a/video-gen-app/src/components/NotificationPopup.tsx +++ b/video-gen-app/src/components/NotificationPopup.tsx @@ -27,7 +27,8 @@ const NotificationPopup: React.FC = () => { const fetchNotifications = useCallback(async () => { try { - const data = await getNotifications(); + const result = await getNotifications(); + const data = result.items || []; const unread = data.filter((n: Notification) => !n.isRead); setNotifications(unread); if (unread.length > 0 && !visible) { diff --git a/video-gen-app/src/pages/CreditRecordsPage.tsx b/video-gen-app/src/pages/CreditRecordsPage.tsx index 00feb02d..ce3f5742 100644 --- a/video-gen-app/src/pages/CreditRecordsPage.tsx +++ b/video-gen-app/src/pages/CreditRecordsPage.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Table, Tag, Empty, Spin, Typography, Row, Col } from 'antd'; +import { Table, Tag, Empty, Spin, Typography, Row, Col, Pagination } from 'antd'; import { WalletOutlined, PlusCircleOutlined, ThunderboltOutlined } from '@ant-design/icons'; import { getCredits } from '../api'; @@ -7,23 +7,31 @@ const CreditRecordsPage: React.FC = () => { const [loading, setLoading] = useState(true); const [records, setRecords] = useState([]); const [credits, setCredits] = useState(0); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize] = useState(10); useEffect(() => { loadData(); - }, []); + }, [page]); const loadData = async () => { setLoading(true); try { - const data = await getCredits(); + const data = await getCredits(page, pageSize); setRecords(data.records || []); setCredits(data.credits || 0); + setTotal(data.total || 0); } catch { setRecords([]); } setLoading(false); }; + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + const columns = [ { title: '变动类型', @@ -139,13 +147,24 @@ const CreditRecordsPage: React.FC = () => { description={暂无积分变动记录} /> ) : ( - + <> +
+
+ +
+ )} diff --git a/video-gen-app/src/pages/MessagesPage.tsx b/video-gen-app/src/pages/MessagesPage.tsx index 199bb00e..015ef539 100644 --- a/video-gen-app/src/pages/MessagesPage.tsx +++ b/video-gen-app/src/pages/MessagesPage.tsx @@ -1,36 +1,54 @@ import React, { useEffect, useState } from 'react'; -import { Table, Tag, Empty, Spin, Typography, Button } from 'antd'; +import { Table, Tag, Empty, Spin, Typography, Button, Pagination } from 'antd'; import { BellOutlined, SettingOutlined, WalletOutlined, GiftOutlined } from '@ant-design/icons'; -import { getNotifications, markNotificationRead } from '../api'; +import { getNotifications, markNotificationRead, getUnreadCount } from '../api'; const MessagesPage: React.FC = () => { const [loading, setLoading] = useState(true); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize] = useState(10); useEffect(() => { loadData(); - }, []); + loadUnreadCount(); + }, [page]); const loadData = async () => { setLoading(true); try { - const data = await getNotifications(); - setNotifications(data || []); - setUnreadCount(data.filter((n: any) => !(n.isRead ?? n.is_read)).length); + const data = await getNotifications(page, pageSize); + setNotifications(data.items || []); + setTotal(data.total || 0); } catch { setNotifications([]); } setLoading(false); }; + const loadUnreadCount = async () => { + try { + const count = await getUnreadCount(); + setUnreadCount(count); + } catch { + setUnreadCount(0); + } + }; + const handleMarkRead = async (id: string) => { try { await markNotificationRead(id); loadData(); + loadUnreadCount(); } catch { /* ignore */ } }; + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + const columns = [ { title: '类型', @@ -131,13 +149,24 @@ const MessagesPage: React.FC = () => { description={暂无消息} /> ) : ( -
+ <> +
+
+ +
+ )} diff --git a/video-gen-app/src/pages/OrderRecordsPage.tsx b/video-gen-app/src/pages/OrderRecordsPage.tsx index 83f03f67..2b74eb15 100644 --- a/video-gen-app/src/pages/OrderRecordsPage.tsx +++ b/video-gen-app/src/pages/OrderRecordsPage.tsx @@ -1,27 +1,35 @@ import React, { useEffect, useState } from 'react'; -import { Table, Tag, Empty, Spin, Typography } from 'antd'; +import { Table, Tag, Empty, Spin, Typography, Pagination } from 'antd'; import { FileTextOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons'; import { getPaymentOrders } from '../api'; const OrderRecordsPage: React.FC = () => { const [loading, setLoading] = useState(true); const [orders, setOrders] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize] = useState(10); useEffect(() => { loadData(); - }, []); + }, [page]); const loadData = async () => { setLoading(true); try { - const data = await getPaymentOrders(); - setOrders(data || []); + const data = await getPaymentOrders(page, pageSize); + setOrders(data.items || []); + setTotal(data.total || 0); } catch { setOrders([]); } setLoading(false); }; + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + const columns = [ { title: '订单号', @@ -140,13 +148,24 @@ const OrderRecordsPage: React.FC = () => { description={暂无订单记录} /> ) : ( -
+ <> +
+
+ +
+ )}