Skip to content

fix(playtest): eliminate request fan-out - #315

Open
huyanxius wants to merge 19 commits into
1024XEngineer:mainfrom
huyanxius:fix/314-playtest-request-fanout
Open

fix(playtest): eliminate request fan-out#315
huyanxius wants to merge 19 commits into
1024XEngineer:mainfrom
huyanxius:fix/314-playtest-request-fanout

Conversation

@huyanxius

Copy link
Copy Markdown
Collaborator

修复 PlayTest 预览台按项目逐个拉取角色造成的请求放大,将角色读取收敛为按页请求,并保持用户归属、分页与发布状态过滤边界。

Why

原实现先读取全部项目,再为每个项目请求一次角色列表;项目数量增长时会形成 N+1 请求,开发环境的 StrictMode 重放还会进一步放大请求量,最终触发全局限流。

Changes

  • GET /characters 增加当前用户全部项目范围的分页读取模式,同时保留原有按项目读取与归属校验。
  • 为前端 characterApis 增加跨项目分页接口,PlayTest 并行读取项目与角色后在本地按 projectId 分组。
  • 增加后端归属/分页/状态过滤回归测试,以及前端 101 条角色仅请求两页的请求数量回归测试。

Implementation

  • 后端通过 CharacterProject join 限定 Project.user_id,不暴露其他用户的角色。
  • 前端沿用完整分页读取逻辑,请求数由项目数量决定改为仅由角色分页数量决定。

Verification

  • cd backend && uv run pytest tests/test_character_api.py:18 passed。
  • cd frontend && npm test -- src/entities/character/index.test.ts src/pages/playtest/entry.test.tsx src/pages/quick-start/service.test.ts:29 passed。
  • cd frontend && npm run typecheck:通过。
  • 定向 Ruff、Oxlint 与格式检查:通过。
  • cd frontend && npm run build:通过。
  • 未执行视觉验证(本 PR 无可见 UI 变化)。
  • 未执行全量门禁(按本次改动影响面使用定向验证)。

Scope

Related Issues

Closes #314
Refs #313

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 14, 2026 9:38am

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已检查固定提交范围 8821b192c43700083850559eadf7419950c96abc...ddd067a8970c9ee5349c000f266d56c226675498,覆盖角色接口/服务归属与分页、PlayTest 跨项目装配、项目/资产入口以及共享布局令牌迁移。后端跨项目查询通过项目归属 join 限定当前用户,前端也改为按项目页与角色页读取并在本地分组;现有按项目读取、状态过滤与分页路径保持独立。

未发现满足审查标准且置信度达到阈值的新增正确性、性能或安全问题。git diff --check 通过;本地未能执行定向测试,因为环境没有 uv,且前端 vitest 二进制不可执行。

The project center needs a calmer asset-first browsing surface.

Render the create entrance, project previews, and empty pixel canvases while resolving real character preview images.

Existing project creation, deletion, pagination, and navigation behavior stays intact.
The redesigned gallery must keep its real API and navigation boundaries explicit.

Cover character preview requests, empty canvases, creation links, deletion, and pagination.

The tests now assert user-visible behavior instead of styling internals.
The workspace heading needs the same concise context used across the product surfaces.

Add the approved tagline directly beneath the existing page title.

No workspace navigation or data-loading behavior changes.
The approved workspace context line should remain part of the rendered heading area.

Assert the tagline alongside the existing navigation behavior.

The focused coverage protects the copy without expanding workspace scope.
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.82716% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
frontend/src/pages/projects/index.tsx 87.87% 1 Missing and 3 partials ⚠️
frontend/src/pages/playtest/entry.tsx 95.83% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Codecov exposed untested project preview fallback branches.

Exercise reference images, first frames, and isolated request failures.

Keep gallery previews resilient without changing production behavior.
Project and Playtest entries need the same visual behavior without duplicated styling.

Extract the editorial entry card with asset-specific artwork and reuse it for project creation.

Both entry points now share sizing, color, tilt, and hover behavior.
The project creation entry must keep its established artwork and accessible link contract.

Assert that the rendered link comes from the shared editorial entry component.

The test protects the project entry while Playtest reuses the same component.
The global Playtest entry should surface real outfits before decorative scene content.

Replace the hero with a filterable cross-project gallery and reuse the shared empty-state entry card.

Cards now expose project context, playable actions, frame totals, and disabled states.
The entry must preserve routing, filtering, pagination, empty states, and truthful playback metadata.

Cover the gallery states and add a zero-frame action fixture that must stay out of playable summaries.

The regression protects both the frozen layout and the workbench playback contract.
Reviewers need stable visual evidence for the frozen Playtest entry design.

Add desktop and narrow-screen captures of the populated global outfit gallery.

The images document the responsive states used for Issue 304 acceptance.
Zero-frame actions are visible metadata but cannot enter Playtest.

Count only actions with real frames in the Workspace outfit summary.

Keep the displayed action total aligned with the playable frame contract.
@huyanxius
huyanxius force-pushed the fix/314-playtest-request-fanout branch from d99aec5 to 412f8f6 Compare August 14, 2026 09:33
The shared asset fixture now includes one zero-frame draft action.

Expect character detail and asset library views to render all three actions.

Align both tests with the existing asset display behavior.
PlayTest needs one paginated character source instead of one request per project.

Add a user-scoped query path while preserving project-scoped authorization.

This bounds character loading and keeps ownership and status filters intact.
The new list mode must not cross user boundaries or lose pagination semantics.

Exercise two owned projects, another user, paging, and published status filtering.

The regression locks the authorization and filtering contract for PlayTest.
The gallery previously fetched characters once for every loaded project.

Load projects and the owned character collection in parallel, then group in memory.

Character traffic now scales with pagination instead of project count.
The frontend needs a typed client for the owned cross-project character endpoint.

Verify page, page size, and status mapping without a project identifier.

The test protects the request contract used by the PlayTest gallery.
Project fan-out can silently return when the gallery data fixture grows.

Model unscoped character pagination and count requests across 101 characters.

The regression requires two character pages regardless of project count.
CharacterApis now includes the owned cross-project list method.

Extend the mutable Quick Start fixture with a compatible paginated result.

Existing Quick Start service tests keep exercising the complete interface.
Codecov counted three formatting-only slice lines as uncovered patch code.

Restore the existing main-branch spacing on those expressions.

Keep the request fan-out diff limited to functional changes.
@minorcell

Copy link
Copy Markdown
Member

@fennoai

这种需要读取多个图的情况,一般行业是怎么做的呀?

@fennoai

fennoai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

结论

一般会把“读取多个图”拆成两层处理:资产元数据请求图片二进制加载,不会把所有事情都交给一次大请求。

  • 列表接口只返回图片 URL/缩略图信息:不要返回 base64 或原图;原图放对象存储/CDN,接口返回 thumbnail_url、尺寸、宽高比、版本号等元数据。
  • 列表页分批取数据:分页或无限滚动;首屏只取当前需要的记录。数据量很大时再加虚拟列表,避免一次渲染几千个卡片。
  • 图片按可见性加载:首屏少量图片使用 eager/高优先级,其余使用 loading="lazy"decoding="async";进入详情页再加载大图或原图。
  • 控制并发而不是无脑 Promise.all:只有在必须用 JS 预加载多张图时,才用带上限的队列(例如同时 4–8 张),并支持取消、失败占位和重试。
  • 使用缩略图与 CDN 转码:按卡片尺寸生成 WebP/AVIF 等资源,配合浏览器缓存、长期缓存 URL 和响应式尺寸,降低带宽与解码压力。

对这次 PR 的对应关系

这次 PR 主要解决的是第一层的 API fan-out:原来按项目逐个请求角色,现在项目和角色各自按页读取,再在前端按 projectId 分组(frontend/src/pages/playtest/entry.tsx:28frontend/src/pages/playtest/entry.tsx:55)。这是正确的方向,也正是“列表元数据批量化”的常见做法。

但它并不等于图片本身只会发两次请求:PlayTest 卡片仍会为每个 previewUrl 渲染 <img>,当前没有显式 loading="lazy"frontend/src/pages/playtest/entry.tsx:201)。浏览器通常会自行调度,但如果未来资产数量明显增长,我会建议下一步:

  1. 给卡片图默认加 loading="lazy"decoding="async"
  2. 对首屏 1–2 张图保留高优先级,其余延迟加载;
  3. 后端/CDN 提供缩略图尺寸参数,详情页再请求原图;
  4. 如果 PlayTest 必须“进入页面即全部可用”,再增加有限并发预加载队列,而不是一次性 Promise.all 下载全部图片。

项目详情主图已经采用 eager + fetchPriority="high",这与“详情页首要图片优先加载”的行业习惯一致(frontend/src/pages/character-detail/index.tsx:194)。所以可以概括为:这次 PR 已经把 API 层从 N+1 收敛到分页批量;图片层则应按首屏优先、其余懒加载、缩略图/CDN、有限并发继续演进。

View job run

@minorcell

Copy link
Copy Markdown
Member

结论

一般会把“读取多个图”拆成两层处理:资产元数据请求图片二进制加载,不会把所有事情都交给一次大请求。

  • 列表接口只返回图片 URL/缩略图信息:不要返回 base64 或原图;原图放对象存储/CDN,接口返回 thumbnail_url、尺寸、宽高比、版本号等元数据。
  • 列表页分批取数据:分页或无限滚动;首屏只取当前需要的记录。数据量很大时再加虚拟列表,避免一次渲染几千个卡片。
  • 图片按可见性加载:首屏少量图片使用 eager/高优先级,其余使用 loading="lazy"decoding="async";进入详情页再加载大图或原图。
  • 控制并发而不是无脑 Promise.all:只有在必须用 JS 预加载多张图时,才用带上限的队列(例如同时 4–8 张),并支持取消、失败占位和重试。
  • 使用缩略图与 CDN 转码:按卡片尺寸生成 WebP/AVIF 等资源,配合浏览器缓存、长期缓存 URL 和响应式尺寸,降低带宽与解码压力。

对这次 PR 的对应关系

这次 PR 主要解决的是第一层的 API fan-out:原来按项目逐个请求角色,现在项目和角色各自按页读取,再在前端按 projectId 分组(frontend/src/pages/playtest/entry.tsx:28frontend/src/pages/playtest/entry.tsx:55)。这是正确的方向,也正是“列表元数据批量化”的常见做法。

但它并不等于图片本身只会发两次请求:PlayTest 卡片仍会为每个 previewUrl 渲染 <img>,当前没有显式 loading="lazy"frontend/src/pages/playtest/entry.tsx:201)。浏览器通常会自行调度,但如果未来资产数量明显增长,我会建议下一步:

  1. 给卡片图默认加 loading="lazy"decoding="async"
  2. 对首屏 1–2 张图保留高优先级,其余延迟加载;
  3. 后端/CDN 提供缩略图尺寸参数,详情页再请求原图;
  4. 如果 PlayTest 必须“进入页面即全部可用”,再增加有限并发预加载队列,而不是一次性 Promise.all 下载全部图片。

项目详情主图已经采用 eager + fetchPriority="high",这与“详情页首要图片优先加载”的行业习惯一致(frontend/src/pages/character-detail/index.tsx:194)。所以可以概括为:这次 PR 已经把 API 层从 N+1 收敛到分页批量;图片层则应按首屏优先、其余懒加载、缩略图/CDN、有限并发继续演进。

View job run

@huyanxius 可以参考一下,一般在面对陌生的问题、没有解决方案的时候,可以问一下 AI 行业的做法是什么。

@huyanxius

Copy link
Copy Markdown
Collaborator Author

结论

一般会把“读取多个图”拆成两层处理:资产元数据请求图片二进制加载,不会把所有事情都交给一次大请求。

  • 列表接口只返回图片 URL/缩略图信息:不要返回 base64 或原图;原图放对象存储/CDN,接口返回 thumbnail_url、尺寸、宽高比、版本号等元数据。
  • 列表页分批取数据:分页或无限滚动;首屏只取当前需要的记录。数据量很大时再加虚拟列表,避免一次渲染几千个卡片。
  • 图片按可见性加载:首屏少量图片使用 eager/高优先级,其余使用 loading="lazy"decoding="async";进入详情页再加载大图或原图。
  • 控制并发而不是无脑 Promise.all:只有在必须用 JS 预加载多张图时,才用带上限的队列(例如同时 4–8 张),并支持取消、失败占位和重试。
  • 使用缩略图与 CDN 转码:按卡片尺寸生成 WebP/AVIF 等资源,配合浏览器缓存、长期缓存 URL 和响应式尺寸,降低带宽与解码压力。

对这次 PR 的对应关系

这次 PR 主要解决的是第一层的 API fan-out:原来按项目逐个请求角色,现在项目和角色各自按页读取,再在前端按 projectId 分组(frontend/src/pages/playtest/entry.tsx:28frontend/src/pages/playtest/entry.tsx:55)。这是正确的方向,也正是“列表元数据批量化”的常见做法。
但它并不等于图片本身只会发两次请求:PlayTest 卡片仍会为每个 previewUrl 渲染 <img>,当前没有显式 loading="lazy"frontend/src/pages/playtest/entry.tsx:201)。浏览器通常会自行调度,但如果未来资产数量明显增长,我会建议下一步:

  1. 给卡片图默认加 loading="lazy"decoding="async"
  2. 对首屏 1–2 张图保留高优先级,其余延迟加载;
  3. 后端/CDN 提供缩略图尺寸参数,详情页再请求原图;
  4. 如果 PlayTest 必须“进入页面即全部可用”,再增加有限并发预加载队列,而不是一次性 Promise.all 下载全部图片。

项目详情主图已经采用 eager + fetchPriority="high",这与“详情页首要图片优先加载”的行业习惯一致(frontend/src/pages/character-detail/index.tsx:194)。所以可以概括为:这次 PR 已经把 API 层从 N+1 收敛到分页批量;图片层则应按首屏优先、其余懒加载、缩略图/CDN、有限并发继续演进。
View job run

@huyanxius 可以参考一下,一般在面对陌生的问题、没有解决方案的时候,可以问一下 AI 行业的做法是什么。

🫡

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: eliminate Playtest entry request fan-out

2 participants