主页瀑布流
主页信息流聚合接口,一次请求返回轮播图、公告、推荐、最近更新、新上架、人气排行等全部板块数据
概述
home-feed 是客户端主页的核心数据接口。它将多个板块的数据查询并行执行后聚合返回,避免客户端发起多次请求。支持按需选择板块和自定义每个板块的返回数量。
| 属性 | 值 |
|---|---|
| Slug | home-feed |
| 方法 | GET |
| JWT 校验 | 否(公开接口) |
| 缓存 | Cache-Control: public, max-age=30, s-maxage=30 |
| CORS | 允许所有来源 |
板块说明
接口支持 6 个独立板块,每个板块对应主页 UI 的一个区域:
| 板块 | 参数名 | 说明 | 数据来源 |
|---|---|---|---|
banners | 轮播图 | 首页顶部轮播横幅 | home_banners 表 |
announcements | 公告 | 系统公告和通知 | announcements 表 |
recommended | 编辑推荐 | 手动标记为推荐的漫画 | comics 表 is_recommended = true |
recent | 最近更新 | 按章节更新时间排序 | get_recent_comics RPC |
newlyAdded | 新上架 | 按漫画创建时间排序 | comics 表 create_at DESC |
popular | 人气排行 | 日/周/月三个维度的排行 | comics 表 popularity_* 字段 |
请求格式
GET /functions/v1/home-feed?include=...&recommendedLimit=...查询参数
Prop
Type
请求示例
GET /functions/v1/home-feed不传任何参数,返回全部板块、默认数量。
GET /functions/v1/home-feed?include=banners,recommended,recent&recommendedLimit=6&recentLimit=20只加载轮播图、推荐和最近更新,自定义数量。
GET /functions/v1/home-feed?include=popular&popularityLimit=20只加载人气排行,每个维度返回 20 条。
响应格式
响应体为 {"data": {...}} 结构,data 中只包含请求的板块。板块的返回顺序固定为:recommended → recent → newlyAdded → popular → banners → announcements,与 include 参数中的书写顺序无关。
漫画对象(Comic)
recommended、recent、newlyAdded 和 popular 板块中每个元素的结构:
Prop
Type
轮播图对象(Banner)
Prop
Type
公告对象(Announcement)
Prop
Type
完整响应示例
{
"data": {
"recommended": [
{
"documentId": "42",
"name": "进击的巨人",
"summary": "那一天,人类终于想起了被支配的恐惧...",
"cover": "https://bucket.xfmanga.top/res/covers/42.webp",
"poster": null,
"popularity": { "daily": 320, "weekly": 1850, "monthly": 6200 },
"categoryName": "热血",
"lockStatus": "free",
"latestChapterTitle": "第139话 向着那棵树",
"latestChapterUpdatedAt": "2026-03-10T08:00:00Z",
"latestChapterDocumentId": null,
"releaseDate": "2009-09-09",
"isFinished": true
}
],
"recent": [],
"newlyAdded": [],
"popular": {
"daily": [],
"weekly": [],
"monthly": []
},
"banners": [
{
"id": "b1a2c3d4",
"title": "新番上线",
"subtitle": "本周热门新作推荐",
"imageUrl": "https://bucket.xfmanga.top/img/banners/spring-2026.webp",
"targetType": "comic",
"targetValue": "42"
}
],
"announcements": [
{
"id": "a1b2c3d4",
"title": "系统维护通知",
"content": "3月20日凌晨2:00-4:00进行系统维护",
"type": "warning",
"actionText": "查看详情",
"actionUrl": "/announcements/a1b2c3d4",
"isDismissible": true,
"priority": 10
}
],
"meta": {
"generatedAt": "2026-03-15T10:00:00.000Z",
"limits": {
"recommended": 12,
"recent": 12,
"newlyAdded": 12,
"popular": 12,
"banners": 5,
"announcements": 3
}
}
}
}核心逻辑
并行查询
所有板块的数据库查询通过 Promise.all 并行执行,互不阻塞。即使客户端请求全部 6 个板块(实际发起约 8 次数据库查询,因为 popular 拆为日/周/月三次),总耗时也接近于单次最慢查询的耗时。
┌─ banners 查询 ─┐
├─ announcements 查询 ─┤
├─ recommended 查询 ─┤
├─ recent RPC 调用 ─┼── Promise.all ── 聚合响应
├─ newlyAdded 查询 ─┤
├─ popular/daily ─┤
├─ popular/weekly ─┤
└─ popular/monthly ─┘板块过滤(include 参数)
通过 include 参数控制返回哪些板块。未被选中的板块不会执行数据库查询,减少不必要的开销。
为了向后兼容,include=latest 会自动映射为 newlyAdded;latestLimit 参数也会被 newlyAddedLimit 的逻辑接收。
限制数量(Limit Clamping)
所有 limit 参数都经过 clamp 处理:非法值回退到默认值,超出最大值则截断到上限。
| 板块 | 默认值 | 最大值 |
|---|---|---|
| recommended | 12 | 20 |
| recent | 12 | 30 |
| newlyAdded | 12 | 30 |
| popular(每个维度) | 12 | 30 |
| banners | 5 | 10 |
| announcements | 3 | 10 |
轮播图与公告的时间窗口
home_banners 和 announcements 都支持 start_at / end_at 时间窗口。查询时只返回当前时间处于窗口内(或窗口字段为空)的记录:
-- 伪代码,实际通过 Supabase client 构建
WHERE is_active = true
AND (start_at IS NULL OR start_at <= now())
AND (end_at IS NULL OR end_at >= now())这意味着可以提前配置轮播图和公告,设定一个未来的 start_at,到时间后自动生效,无需手动操作。
最近更新(recent)
此板块使用数据库 RPC 函数 get_recent_comics 而非直接查表:
-- get_recent_comics 函数定义
SELECT c.id, c.title, c.summary, c.cover_url, c.poster_url,
c.lock_status, cat.name as category_name,
c.latest_chapter_title, c.latest_chapter_updated_at
FROM comics c
LEFT JOIN categories cat ON c.category_id = cat.id
WHERE c.latest_chapter_updated_at IS NOT NULL
ORDER BY c.latest_chapter_updated_at DESC
LIMIT limit_count;使用 RPC 的原因是需要 JOIN categories 表获取分类名称,并且只返回有章节更新记录的漫画(latest_chapter_updated_at IS NOT NULL)。
人气排行(popular)
人气排行板块按三个时间维度分别查询,响应中以嵌套结构返回:
{
"popular": {
"daily": [/* 按 popularity_daily 降序 */],
"weekly": [/* 按 popularity_weekly 降序 */],
"monthly": [/* 按 popularity_monthly 降序 */]
}
}popularity_daily、popularity_weekly、popularity_monthly 字段需要由定时任务配合 RPC 函数计算和更新,home-feed 只负责读取。
数据映射
原始数据库字段通过 mapComic 函数映射为客户端友好的驼峰命名结构。其中 summary 字段会进行 HTML 标签剥离和截断处理(最长 120 字符),避免传输过多数据:
const stripAndTruncateSummary = (value: unknown, maxLength = 120) => {
// 1. 剥离 HTML 标签
// 2. 合并多余空白
// 3. 超过 maxLength 截断并加 "..."
}关联数据表
home_banners
| 字段 | 类型 | 说明 |
|---|---|---|
id | uuid | 主键 |
title | text | 标题 |
subtitle | text | 副标题 |
image_url | text | 横幅图片地址 |
target_type | text | 跳转类型,默认 "comic" |
target_value | text | 跳转目标(漫画 ID 或 URL) |
sort_order | integer | 排序权重(降序),默认 0 |
is_active | boolean | 是否启用,默认 true |
start_at | timestamptz | 生效开始时间(null 表示立即生效) |
end_at | timestamptz | 生效结束时间(null 表示永不过期) |
announcements
| 字段 | 类型 | 说明 |
|---|---|---|
id | uuid | 主键 |
title | text | 公告标题 |
content | text | 公告摘要内容 |
detail_content | text | 公告详情正文 |
announcement_type | text | 类型(info / warning / error),默认 "info" |
action_text | text | 操作按钮文字 |
action_url | text | 操作按钮链接 |
priority | integer | 优先级(降序排列),默认 0 |
is_active | boolean | 是否启用,默认 true |
is_dismissible | boolean | 可否关闭,默认 true |
is_popup | boolean | 是否弹窗展示,默认 false |
start_at | timestamptz | 生效开始时间 |
end_at | timestamptz | 生效结束时间 |
comics(主要字段)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
title | text | 漫画标题 |
summary | text | 简介(可能含 HTML) |
cover_url | text | 封面图地址 |
poster_url | text | 海报图地址 |
lock_status | text | 锁定状态,默认 "free" |
is_recommended | boolean | 是否编辑推荐 |
popularity_daily | integer | 日人气值 |
popularity_weekly | integer | 周人气值 |
popularity_monthly | integer | 月人气值 |
category_id | bigint | 关联 categories 表 |
latest_chapter_title | text | 最新章节标题(冗余字段) |
latest_chapter_updated_at | timestamptz | 最新章节更新时间(冗余字段) |
is_finished | boolean | 是否完结 |
release_date | date | 发布日期 |
create_at / created_at | timestamptz | 创建时间 |
latest_chapter_title 和 latest_chapter_updated_at 是从 chapters 表冗余到 comics 表的字段,在新章节发布时同步更新,避免主页查询时 JOIN chapters 表。
缓存策略
响应头设置了 Cache-Control: public, max-age=30, s-maxage=30,意味着:
- CDN 和浏览器都会缓存响应 30 秒
- 30 秒内的重复请求直接由缓存返回,不会到达 Edge Function
- 适合主页这种高频访问但数据不需要实时更新的场景
错误响应
{
"error": {
"code": "HOME_FEED_ERROR",
"message": "具体错误信息"
}
}任何一个板块查询失败都会导致整个请求返回 500。这是因为 Promise.all 的 fail-fast 特性——如果需要更高的容错性,可以考虑改为 Promise.allSettled 并对失败的板块返回空数组。
客户端调用示例
suspend fun loadHomeFeed(
sections: List<String> = emptyList(),
): HomeFeedResponse {
val url = buildString {
append("${supabaseUrl}/functions/v1/home-feed")
if (sections.isNotEmpty()) {
append("?include=${sections.joinToString(",")}")
}
}
val response = httpClient.get(url) {
header("apikey", supabaseAnonKey)
}
return response.body()
}
// 首次进入主页:加载全部板块
val feed = loadHomeFeed()
// 切换到排行榜 Tab:只加载排行
val ranking = loadHomeFeed(listOf("popular"))async function fetchHomeFeed(
sections?: string[],
limits?: Record<string, number>
) {
const params = new URLSearchParams()
if (sections?.length) {
params.set('include', sections.join(','))
}
if (limits) {
for (const [key, value] of Object.entries(limits)) {
params.set(key, String(value))
}
}
const url = `${SUPABASE_URL}/functions/v1/home-feed?${params}`
const res = await fetch(url, {
headers: { apikey: SUPABASE_ANON_KEY },
})
if (!res.ok) throw new Error(`Home feed error: ${res.status}`)
return res.json()
}
// 完整加载
const { data } = await fetchHomeFeed()
// 按需加载 + 自定义数量
const { data } = await fetchHomeFeed(
['recommended', 'recent'],
{ recommendedLimit: '6', recentLimit: '20' }
)