perf(core): 资产/通知「取全部」改大页+并行,根治每次刷新串十几个请求

资产接口原固定 20/页且不支持 page_size,前端 allAssets 逐页串行翻完(190 资产=10 个
串行请求,每个 0.5~1.7s 依次等)→ 整页刷新好几秒。
- 后端:AssetViewSet 加 AssetPagination(page_size_query_param,上限 200)
- 前端:allAssets/allNotifications 改「首页大页(200/100)+ 剩余页 Promise.all 并行」
资产 ≤200 时 1 个请求取全;再多也并行不串行。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 14:52:19 +08:00
co-authored by Claude Opus 4.8
parent 608c8aeb7f
commit da0c7edfe2
2 changed files with 41 additions and 18 deletions
+10
View File
@@ -6,6 +6,7 @@ from django.db import transaction
from django.http import StreamingHttpResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -18,9 +19,18 @@ from .serializers import AssetSerializer, AssetUploadSerializer
from .storage import TosStorage
class AssetPagination(PageNumberPagination):
"""允许前端用 ?page_size= 覆盖(默认 20,上限 200)。前端「取全部资产」一次大页拉完,
不再逐页串行(原来 20/页 → 资产多时每次刷新要串十几个请求,整页很慢)。"""
page_size_query_param = "page_size"
max_page_size = 200
class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
queryset = Asset.objects.prefetch_related("files").all()
serializer_class = AssetSerializer
pagination_class = AssetPagination
search_fields = ["name", "description"]
ordering_fields = ["created_at", "updated_at", "name"]
+31 -18
View File
@@ -373,13 +373,21 @@ export const api = {
// 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url,
// 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。
async allAssets(): Promise<Asset[]> {
const out: Asset[] = [];
let path = "/api/assets/";
for (let guard = 0; guard < 50 && path; guard += 1) {
const page: Paginated<Asset> = await request<Paginated<Asset>>(path);
out.push(...page.results);
path = page.next ? new URL(page.next).pathname + new URL(page.next).search : "";
}
// 一次大页(page_size=200)尽量取全;若还有多页,并行取剩余页(不再逐页串行等,根治整页刷新慢)
const SIZE = 200;
const first = await request<Paginated<Asset>>(`/api/assets/?page_size=${SIZE}`);
const out: Asset[] = [...first.results];
const pageSize = first.results.length || SIZE; // 后端若忽略 page_size,按实际页大小算页数,逻辑仍正确
const totalPages = pageSize > 0 ? Math.ceil((first.count || out.length) / pageSize) : 1;
if (totalPages <= 1) return out;
const rest = await Promise.all(
Array.from({ length: totalPages - 1 }, (_, i) =>
request<Paginated<Asset>>(`/api/assets/?page_size=${SIZE}&page=${i + 2}`)
.then((p) => p.results)
.catch(() => [] as Asset[])
)
);
rest.forEach((r) => out.push(...r));
return out;
},
uploadAsset(formData: FormData) {
@@ -423,18 +431,23 @@ export const api = {
},
// 跟着 next 把所有消息取全(给侧边栏徽标 + 团队动态用,不参与收件箱滚动渲染),与 allAssets 同套路
async allNotifications(): Promise<NotificationList> {
const out: Notification[] = [];
let path = "/api/ops/notifications/?page_size=100";
let unreadCount = 0;
let typeCounts: NotificationList["type_counts"];
for (let guard = 0; guard < 100 && path; guard += 1) {
const page = await request<NotificationList>(path);
out.push(...page.results);
unreadCount = page.unread_count;
typeCounts = page.type_counts ?? typeCounts;
path = page.next ? new URL(page.next).pathname + new URL(page.next).search : "";
// 首页拿计数,剩余页并行(原串行逐页:消息多时侧边栏徽标加载也拖慢整页)
const SIZE = 100;
const first = await request<NotificationList>(`/api/ops/notifications/?page_size=${SIZE}`);
const out: Notification[] = [...first.results];
const pageSize = first.results.length || SIZE;
const totalPages = pageSize > 0 ? Math.ceil((first.count || out.length) / pageSize) : 1;
if (totalPages > 1) {
const rest = await Promise.all(
Array.from({ length: totalPages - 1 }, (_, i) =>
request<NotificationList>(`/api/ops/notifications/?page_size=${SIZE}&page=${i + 2}`)
.then((p) => p.results)
.catch(() => [] as Notification[])
)
);
rest.forEach((r) => out.push(...r));
}
return { count: out.length, next: null, previous: null, results: out, unread_count: unreadCount, type_counts: typeCounts };
return { count: out.length, next: null, previous: null, results: out, unread_count: first.unread_count, type_counts: first.type_counts };
},
markAllNotificationsRead() {
return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", {