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:
@@ -6,6 +6,7 @@ from django.db import transaction
|
|||||||
from django.http import StreamingHttpResponse
|
from django.http import StreamingHttpResponse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
from rest_framework.parsers import FormParser, MultiPartParser
|
from rest_framework.parsers import FormParser, MultiPartParser
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -18,9 +19,18 @@ from .serializers import AssetSerializer, AssetUploadSerializer
|
|||||||
from .storage import TosStorage
|
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):
|
class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||||
queryset = Asset.objects.prefetch_related("files").all()
|
queryset = Asset.objects.prefetch_related("files").all()
|
||||||
serializer_class = AssetSerializer
|
serializer_class = AssetSerializer
|
||||||
|
pagination_class = AssetPagination
|
||||||
search_fields = ["name", "description"]
|
search_fields = ["name", "description"]
|
||||||
ordering_fields = ["created_at", "updated_at", "name"]
|
ordering_fields = ["created_at", "updated_at", "name"]
|
||||||
|
|
||||||
|
|||||||
+31
-18
@@ -373,13 +373,21 @@ export const api = {
|
|||||||
// 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url,
|
// 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url,
|
||||||
// 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。
|
// 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。
|
||||||
async allAssets(): Promise<Asset[]> {
|
async allAssets(): Promise<Asset[]> {
|
||||||
const out: Asset[] = [];
|
// 一次大页(page_size=200)尽量取全;若还有多页,并行取剩余页(不再逐页串行等,根治整页刷新慢)
|
||||||
let path = "/api/assets/";
|
const SIZE = 200;
|
||||||
for (let guard = 0; guard < 50 && path; guard += 1) {
|
const first = await request<Paginated<Asset>>(`/api/assets/?page_size=${SIZE}`);
|
||||||
const page: Paginated<Asset> = await request<Paginated<Asset>>(path);
|
const out: Asset[] = [...first.results];
|
||||||
out.push(...page.results);
|
const pageSize = first.results.length || SIZE; // 后端若忽略 page_size,按实际页大小算页数,逻辑仍正确
|
||||||
path = page.next ? new URL(page.next).pathname + new URL(page.next).search : "";
|
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;
|
return out;
|
||||||
},
|
},
|
||||||
uploadAsset(formData: FormData) {
|
uploadAsset(formData: FormData) {
|
||||||
@@ -423,18 +431,23 @@ export const api = {
|
|||||||
},
|
},
|
||||||
// 跟着 next 把所有消息取全(给侧边栏徽标 + 团队动态用,不参与收件箱滚动渲染),与 allAssets 同套路
|
// 跟着 next 把所有消息取全(给侧边栏徽标 + 团队动态用,不参与收件箱滚动渲染),与 allAssets 同套路
|
||||||
async allNotifications(): Promise<NotificationList> {
|
async allNotifications(): Promise<NotificationList> {
|
||||||
const out: Notification[] = [];
|
// 首页拿计数,剩余页并行(原串行逐页:消息多时侧边栏徽标加载也拖慢整页)
|
||||||
let path = "/api/ops/notifications/?page_size=100";
|
const SIZE = 100;
|
||||||
let unreadCount = 0;
|
const first = await request<NotificationList>(`/api/ops/notifications/?page_size=${SIZE}`);
|
||||||
let typeCounts: NotificationList["type_counts"];
|
const out: Notification[] = [...first.results];
|
||||||
for (let guard = 0; guard < 100 && path; guard += 1) {
|
const pageSize = first.results.length || SIZE;
|
||||||
const page = await request<NotificationList>(path);
|
const totalPages = pageSize > 0 ? Math.ceil((first.count || out.length) / pageSize) : 1;
|
||||||
out.push(...page.results);
|
if (totalPages > 1) {
|
||||||
unreadCount = page.unread_count;
|
const rest = await Promise.all(
|
||||||
typeCounts = page.type_counts ?? typeCounts;
|
Array.from({ length: totalPages - 1 }, (_, i) =>
|
||||||
path = page.next ? new URL(page.next).pathname + new URL(page.next).search : "";
|
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() {
|
markAllNotificationsRead() {
|
||||||
return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", {
|
return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", {
|
||||||
|
|||||||
Reference in New Issue
Block a user