01 系统架构设计

分册定位:本册是《播客目录展示服务》交付文档的架构总览,面向工程师与 AI Agent,描述实际落地的三端系统(数据底座 / 服务端 / 桌面客户端)。 真源对照:架构决策真源为 deepresearch_podcast-catalog-api-architecture_20260604_v4.md(下称 v4);实际部署见 DELIVERY.md;依赖链与运维见 docs/SERVICE-OPS.md。 真实性约定:所有"已落地"结论均给出 file_path:line 锚点;v4 设计但本项目未做/简化的,统一在 §7 标注,正文不臆造。 最后更新:2026-06-11。


0. 一页速览(TL;DR)

维度实际落地结论锚点
形态只读播客目录 + macOS 桌面客户端浏览/搜索/播放,单机内网部署(服务端 IP 10.10.20.207)DELIVERY.md:3、docs/SERVICE-OPS.md:2
数据底座PostgreSQL 16(容器 podcast-pg),8 张表 / 17 索引docs/SERVICE-OPS.md:23、scripts/etl/finalize.sql:11
数据规模293,781 播客 + 22,867,469 剧集 + 354,508 分类关联实测 podcast-pg 库(docs/SERVICE-OPS.md:23)
服务端Next.js 16.2.7 App Router · 仅 Route Handlers · Node runtime;13 个路由 / 4 个 servicepodcast-api/package.json:21、src/app/api/**/route.ts
客户端Qt6 主窗口内嵌 CEF(Chromium Embedded Framework)浏览器,加载内嵌 Web UI(静态 HTML/CSS/JS)desktop/README.md:1、podcast-api/public/app/index.html
鉴权内网固定 Bearer token 白名单(API_BEARER_TOKENS,默认 dev-token),userId = tokensrc/lib/api/auth.ts:36、src/config/env.ts:10
缓存进程内 TTL memo(非 Redis),单实例部署src/lib/api/memo.ts:1
音频不代理,直接返回外部 CDN audioUrl(v4 D-5)src/lib/services/catalog.service.ts:769

与 v4 最大的落地差异:v4 推荐 k8s 多副本容器 + Redis 7 + ioredis + JWT/API Key 双层鉴权;本项目落地为 单机 colima 容器 + PostgreSQL 单库 + 进程内缓存 + 固定 token 内网鉴权。v4 中框架无关的契约(数据库 Schema、REST/keyset 分页、错误码、音频不代理)原样落地;v4 中为公网高并发设计的部分(Redis、k8s、JWT 轮换、CORS)按"内网单机"现实裁掉或降级。


1. 项目定位与全景

1.1 一句话定位

为公司内网提供一个只读的播客目录展示与播放服务:服务端把一份离线构建的播客数据集(dataset.7z)灌入数据库并以 REST API 暴露;macOS 桌面客户端通过 HTTP API 浏览分类、搜索播客、查看剧集、并直接从外部 CDN 播放音频。

1.2 数据形态(来自 v4,已核验)

  • 数据是批量构建的只读快照(buildId),偏只读、周期重建(deepresearch...v4.md:51)。
  • 剧集是大表:audioUrl 指向外部 CDN(如 traffic.libsyn.com),非自托管(deepresearch...v4.md:66)。
  • 分类体系沿用 Apple Podcasts 同款分类法,主类用 -- 表示复合词(如 health--fitness)(deepresearch...v4.md:71)。

1.3 部署形态(来自 DELIVERY,实际落地)

  • 方案 B · 内网服务端模式:本机(10.10.20.207)作为内网服务端,同事安装 PodcastDesktop.dmg,app 通过 HTTP API 取数据并展示/播放(DELIVERY.md:3)。
  • 客户端范围:仅 Apple Silicon (arm64) / macOS;Intel / Windows 不在本次交付范围(DELIVERY.md:19)。

1.4 三端职责划分

端技术职责不负责
数据底座PostgreSQL 16(容器)持久化只读目录 + 用户态(播放进度 / 下载任务);索引化筛选/搜索/分页不直接对外,只绑 127.0.0.1
服务端Next.js 16 Route HandlersREST API(/api/v1/podcasts/*)+ 静态 Web UI(/app/);鉴权、校验、日志、缓存不代理音频;不签发 JWT
客户端Qt6 + CEF + 内嵌 Web UI原生窗口壳 + Chromium 渲染 Web UI + 浏览器内音频解码播放不直接连数据库;不做业务逻辑(逻辑在 Web UI/服务端)

2. 三端总体架构图

┌──────────────────────────────────────────────────────────────────────────────┐
│  桌面客户端 (PodcastDesktop.app, macOS arm64)                                   │
│  ┌────────────────────────────┐   嵌入   ┌──────────────────────────────────┐  │
│  │ Qt6 主窗口 (QMainWindow)    │ ───────▶ │ CEF (Chromium Embedded Framework)│  │
│  │ desktop/src/qt_window.cpp   │          │ + 5 个 Helper 进程               │  │
│  │ + 10ms QTimer 驱动消息泵     │          │ desktop/src/cef_client.cc        │  │
│  └────────────────────────────┘          │   加载 → http://10.10.20.207:3000 │  │
│                                          │          /app/index.html (Web UI) │  │
│                                          │   音频在 Chromium 内解码播放      │  │
│                                          └──────────────────────────────────┘  │
└───────────────────────────────┬──────────────────────────────────────────────┘
                                 │ HTTP (Authorization: Bearer <token>)
                                 │ LAN: http://10.10.20.207:3000
                                 ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│  服务端  Next.js 16.2.7 (App Router · 仅 Route Handlers · Node runtime)         │
│  监听 *:3000(全部网卡,LAN 可达)        工作目录 podcast-api/                  │
│                                                                                │
│   静态 Web UI  /app/*  ──────────────▶ public/app/{index.html,app.js,styles.css}│
│                                                                                │
│   REST API     /api/health                                                     │
│                /api/v1/podcasts/*   (13 个 route.ts)                            │
│        ┌─────────────────────────────────────────────────────────────┐        │
│        │ route.ts: authenticate() → zod 校验 → service → envelope(ok/fail)│      │
│        │   lib/api/auth.ts  lib/validations/*  lib/api/envelope.ts       │      │
│        └───────────────────────────────┬─────────────────────────────┘        │
│   4 个 service (lib/services/*)         │  进程内 TTL memo (lib/api/memo.ts)    │
│   catalog / playback / downloads / health                                      │
│                                         │ Drizzle ORM + postgres-js (连接池 max:10)│
└─────────────────────────────────────────┼─────────────────────────────────────┘
                                          │ DATABASE_URL → 127.0.0.1:5432
                                          ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│  数据底座  PostgreSQL 16  (容器 podcast-pg, 只绑 127.0.0.1:5432)                 │
│  在 colima VM 内运行;VM 磁盘镜像位于外置盘 /Volumes/External/develop/.colima    │
│                                                                                │
│  8 张表:podcasts / episodes / podcast_categories                              │
│          playback_progress / download_tasks                                    │
│          language_stats / category_stats / category_sub_stats (ETL 预聚合)     │
│  17 索引:GIN(tsv 全文) · GIN(name trgm) · keyset(popular/name/recent) · ...     │
└──────────────────────────────────────────────────────────────────────────────┘
        ▲
        │ 离线 ETL(约 20 分钟一次性入库)
   dataset.7z → transform.mjs → COPY staging → finalize.sql → perf-stats.sql

  音频:客户端拿 episode 的 audioUrl,由 Chromium 直连外部 CDN(默认不经服务端)

锚点:客户端结构 desktop/README.md:1、desktop/src/qt_window.cpp、desktop/src/cef_client.cc;服务端监听 docs/SERVICE-OPS.md:14;DB 只绑本地 docs/SERVICE-OPS.md:12、DELIVERY.md:36;依赖链 docs/SERVICE-OPS.md:9。


3. 技术栈选型与裁决

3.1 实际技术栈(来自 podcast-api/package.json)

类别选型版本锚点
Web 框架Next.js(App Router,仅 Route Handlers)16.2.7package.json:23
UI 运行时React / React-DOM19.2.4package.json:25
ORMDrizzle ORM^0.45.2package.json:21
DB 驱动postgres(postgres-js)^3.4.9package.json:24
校验Zod^4.4.3package.json:27
日志pino + pino-pretty(dev)^10.3.1package.json:22
测试Vitest + @vitest/coverage-v8^4.1.8package.json:53
语言TypeScript^5package.json:50
Nodeengines>=22package.json:7

注:v4 执行摘要写的是 next@16.2.6 / drizzle@0.38 / zod@3.24 / pino@9 / ioredis@5.10,实际落地版本更新且用 postgres-js 取代 node-postgres、用进程内 memo 取代 ioredis/Redis。版本以 package.json 为准。

3.2 逐项裁决

Next.js 16(App Router · 仅 Route Handlers)— 公司钦定栈

  • 为什么:v4 §3.1 明确"公司钦定 Next.js 栈",覆盖早期 Go/Hono 备选(deepresearch...v4.md:19)。官方支持纯 API 后端,route.ts 嵌套实现 /v1 路径版本化。
  • 落地证据:13 个 route.ts,全部为命名导出 export async function GET/POST(...),统一 export const dynamic = 'force-dynamic'(如 src/app/api/v1/podcasts/shows/route.ts:11、src/app/api/health/route.ts:6)。
  • runtime:全部跑默认 Node.js runtime(未声明 runtime='edge'),因为 postgres-js / pino 依赖 Node 核心模块——与 v4 §3.2 的运行时事实链一致(deepresearch...v4.md:109)。

Drizzle ORM + PostgreSQL 16

  • 为什么 PG:v4 §2 论证"必须要数据库"——~2287 万剧集要按分类/语言/关键词做有索引筛选 + 分页,裸 JSONL 全表扫描不可行(deepresearch...v4.md:83)。
  • 为什么 Drizzle:官方有 PostgreSQL Full-Text Search 与 Cursor-based Pagination 指南。落地中全文检索用 sql\`片段表达websearch_to_tsquery/ts_rank(src/lib/services/catalog.service.ts:371、:377);keyset 用行值比较 (col, id) < (v, id)(catalog.service.ts:398`)。
  • schema 即 DDL 镜像:src/lib/db/schema.ts 注释明确"mirroring scripts/etl/finalize.sql",索引由 ETL 创建,API 从不在目录表 INSERT(schema.ts:1、schema.ts:13)。

postgres-js(取代 v4 的 node-postgres)

  • 落地:src/lib/db/client.ts:15 用 postgres(env.DATABASE_URL, { max: 10 }) 建常驻连接池,drizzle 绑定该 client(client.ts:24)。dev HMR 下复用全局单例避免连接堆积(client.ts:8)。
  • 与 v4 差异:v4 文中以 node-postgres 为例论证"Workers 跨请求隔离不兼容",本项目在 Node 容器内不受此限,且换用更轻的 postgres-js。两者都是常驻连接池模型,结论一致(容器内直连、无需 Hyperdrive)。

Zod 4 / pino 10 / Vitest 4

  • Zod:每端点入口校验,searchParamsToObject(req.nextUrl) → schema.safeParse(...),失败映射业务码 1000(src/app/api/v1/podcasts/shows/route.ts:25、src/lib/api/errors.ts:12)。
  • pino:单例 logger,dev 用 pino-pretty、prod 输出 JSON 给采集层;注释强制"禁 console.*"(src/lib/logger.ts:1、:12)。
  • Vitest:npm run test / test:coverage 已配置(package.json:14)。

CEF(Chromium Embedded Framework)— 客户端壳

  • 为什么 CEF 而非 Qt WebEngine:desktop/README.md:6 明确"音频播放发生在 Chromium 内——不使用 Qt WebEngine"。CEF 提供完整 Chromium 渲染 + 媒体解码,且发行版扁平布局与 Chrome/Electron 同款、公证兼容(DELIVERY.md:16)。
  • 集成方式:Qt6 主窗口把 CEF 浏览器作为中央控件的子 NSView 嵌入(SetAsChild),10ms QTimer 驱动 CefDoMessageLoopWork();CEF 版本 144,5 个 Helper 变体(desktop/README.md Architecture 段)。

3.3 与 v4 "k8s 方案" 对照本项目"本机内网"落地的差异

维度v4 设计(为公网高并发)本项目落地(本机内网)取舍理由
部署k8s 容器 output: standalone,多副本 + HPA(deepresearch...v4.md:131)单机 colima VM 内 1 个 PG 容器 + 1 个 npm run start 进程(docs/SERVICE-OPS.md:9)内网同事级并发,单机够用
入口Ingress/Gateway TLS 终止 + 限流粗筛(deepresearch...v4.md:127)无网关,Next 直接监听 *:3000,HTTP 明文(docs/SERVICE-OPS.md:14)内网可信,省去 TLS/网关
缓存Redis 7 + ioredis 常驻单例(deepresearch...v4.md:101)进程内 Map + TTL(src/lib/api/memo.ts:9)单实例无需跨进程共享缓存
鉴权API Key + JWT(access 短期 + refresh 轮换)或复用 login token(deepresearch...v4.md:34)固定 Bearer token 白名单,userId=token(src/lib/api/auth.ts:48)内网固定 token,未做用户体系
搜索PG 全文检索起步,可升级 Meilisearch/Typesense(deepresearch...v4.md:271)PG 全文检索(tsv + GIN),未引入独立搜索引擎元数据级搜索,PG 够用
DB 暴露容器内直连,PG 可外部托管PG 容器只绑 127.0.0.1:5432,LAN 扫不到(DELIVERY.md:36)内网安全收敛

4. 部署拓扑

4.1 依赖链(启动顺序,来自 SERVICE-OPS)

外置盘挂载 (/Volumes/External)               ← 一切的前提
  └─ colima 虚拟机 (4C/6G, docker 运行时)     ← VM 镜像在外置盘 .colima(经 ~/.colima 符号链接)
       └─ podcast-pg 容器 (PostgreSQL 16)     ← 只绑 127.0.0.1:5432,数据卷 podcast-pgdata
            └─ Next.js 生产服务 (端口 3000)    ← 监听全部网卡,LAN 可达
                 ├─ REST API  /api/v1/*
                 └─ Web UI    /app/index.html  ← 桌面端加载的页面

锚点:docs/SERVICE-OPS.md:9。为什么是这个顺序:DB 镜像在外置盘上的 colima VM 磁盘里,必须先有外置盘 → 再有 VM → 再有 DB 容器,最后 Next 才能连库(docs/SERVICE-OPS.md:18)。

4.2 拓扑要点表

组件配置锚点
colimaDocker 运行时 VM(4C/6G),镜像 /Volumes/External/develop/.colimadocs/SERVICE-OPS.md:22
podcast-pgPostgreSQL 16,--restart unless-stopped(VM 在则自动拉起),只绑 127.0.0.1(已验证 LAN 扫不到 5432)docs/SERVICE-OPS.md:23、DELIVERY.md:36
Next.js生产构建 npm run start,工作目录 podcast-api/,日志 /tmp/podcast-prod.logdocs/SERVICE-OPS.md:24
监听API/UI 监听 *:3000,LAN 内 http://10.10.20.207:3000 可达(已从 LAN IP 验证 200)DELIVERY.md:37
防火墙开启 macOS 防火墙时需放行 node 的 3000 端口入站DELIVERY.md:39

4.3 一键启停

  • 启动:podcast-api/scripts/serve.sh(幂等)——colima → podcast-pg(等 pg_isready)→ 无 .next 先 npm run build → npm run start → 回显本机与 LAN 健康检查(podcast-api/scripts/serve.sh:1,末行输出 READY — clients connect to: http://10.10.20.207:3000/app/index.html)。
  • 停止:scripts/stop.sh(--vm 连 VM 一起停,拔外置盘前必须)(docs/SERVICE-OPS.md:71)。

4.4 客户端服务地址解析优先级

桌面端按以下顺序解析服务端地址(DELIVERY.md:21):

序来源
1--url=<完整地址> 命令行参数
2环境变量 PODCAST_SERVER_URL
3~/Library/Application Support/Podcast Desktop/server_url.txt(用户机覆盖)
4app 内置 Contents/Resources/server_url.txt(打包烧录,当前 = http://10.10.20.207:3000/app/index.html)
5兜底 localhost:3000

IP 变更只需重打包(SERVER_URL=... ./package.sh)或让用户按 ③ 放一行文本文件,无需重装(DELIVERY.md:28)。


5. 端到端数据流

5.1 离线入库链(一次性,约 20 分钟)

dataset.7z
  └─(解压)→ dataset/ JSONL
       └─ scripts/etl/transform.mjs   逐行解析,脏行容错跳过并计数 → 三份 TSV
            └─ COPY → *_staging 表
                 └─ scripts/etl/finalize.sql   staging → 最终表(DISTINCT ON 去重、
                 │     外键约束、孤儿行丢弃计数)→ 批量后建索引 → ANALYZE
                 └─ scripts/etl/perf-stats.sql  预聚合 language_stats / category_stats /
                       category_sub_stats + 3 个 keyset 索引

锚点:ETL 目录 podcast-api/scripts/etl/;入库步骤 finalize.sql:1("staging → final tables ... then indexes (after bulk load), then ANALYZE");预聚合表 perf-stats.sql:8;重建入口 reload.sh / run-etl.sh(docs/SERVICE-OPS.md:135)。

预聚合的工程意义:目录是只读快照,语言/分类计数在入库时算一次存进 *_stats 表,API 直接读,避免每请求 count(DISTINCT) 跨 join(src/lib/db/schema.ts:115、listLanguages 走 language_stats 见 catalog.service.ts:252)。

5.2 在线读取链(每请求)

桌面端 CEF / Web UI
  └─ fetch(http://10.10.20.207:3000/api/v1/podcasts/shows?..., Authorization: Bearer <token>)
       └─ route.ts:  authenticate(req)            鉴权 → 失败 401 / code 1001
       │             zod.safeParse(searchParams)  校验 → 失败 400 / code 1000
       │             decodeCursor + 类型守卫       游标合法性
       └─ catalog.service.listShows(...)          Drizzle 构造 SQL(含 keyset / tsv)
            └─ postgres-js 连接池 → PostgreSQL 16  走 GIN / keyset 索引
       └─ envelope.ok(data, requestId)            { code:0, message:"OK", data, request_id }
  └─ Web UI 渲染列表/详情
  └─ 播放:取 episode.audioUrl → Chromium 直连外部 CDN(不经服务端)

锚点:鉴权 src/lib/api/auth.ts:36;校验/游标 src/app/api/v1/podcasts/shows/route.ts:25;service catalog.service.ts:356;统一信封 src/lib/api/envelope.ts:11;音频直连 catalog.service.ts:769(D-5)。

5.3 响应信封与错误码(实际落地)

成功:{ code: 0, message: "OK", data, request_id }(envelope.ts:11)。错误码(src/lib/api/errors.ts:8):

codeHTTP含义
0200成功
1000400参数校验失败(Zod 详情入 message)
1001401token 缺失或无效
1003403试用配额耗尽(终身 3 集)
1004403当日下载配额超限(300 集/日)
2001404show/episode/task 未找到
2002403内容不可下载(无 audio_url)
3001500服务端内部错误

5.4 API 路由全景(13 个,已核验)

路由方法service锚点
/api/healthGEThealthsrc/app/api/health/route.ts
/api/v1/podcasts/languagesGETcatalog.../languages/route.ts
/api/v1/podcasts/categoriesGETcatalog.../categories/route.ts
/api/v1/podcasts/showsGETcatalog(offset/keyset 混合).../shows/route.ts
/api/v1/podcasts/shows/{showId}GETcatalog.../shows/[showId]/route.ts
/api/v1/podcasts/shows/{showId}/episodesGETcatalog(keyset).../shows/[showId]/episodes/route.ts
/api/v1/podcasts/episodes/{episodeId}GETcatalog.../episodes/[episodeId]/route.ts
/api/v1/podcasts/episodes/{episodeId}/streamGETcatalog(D-5 直连 CDN).../episodes/[episodeId]/stream/route.ts
/api/v1/podcasts/playback/progressPOSTplayback(UPSERT).../playback/progress/route.ts
/api/v1/podcasts/playback/progress/{episodeId}GETplayback.../playback/progress/[episodeId]/route.ts
/api/v1/podcasts/downloadsPOSTdownloads(配额 + D-6 立即完成).../downloads/route.ts
/api/v1/podcasts/downloads/{taskId}GETdownloads.../downloads/[taskId]/route.ts
/api/v1/user/quota/podcastsGETdownloads.../user/quota/podcasts/route.ts

注:API 基路径是 /api/v1/podcasts/...(业务前缀 podcasts),与 v4 文中示例的 /v1/... 路径不同;以代码为准。


6. 关键架构决策与权衡表

#决策点选择权衡 / 理由锚点
D1shows 列表分页稳定排序用 keyset、q 相关性用 offset三种稳定排序(popular/name/recent)走复合索引行值比较,任意页恒定耗时;q 搜索按 ts_rank 排序无稳定 keyset,回退 offsetcatalog.service.ts:33、:381、:398
D2剧集列表分页keyset on (publish_date, episode_uid)单档最高 1500 条且增量更新,keyset 翻页恒定、不错位;NULL 日期行用 null 标记排在末尾catalog.service.ts:583、:617
D3音频是否代理不代理(D-5)直接返回外部 CDN audioUrl,stream_url 即原始 url;代理吃带宽增延迟,对"只展示"不值catalog.service.ts:769、deepresearch...v4.md:547
D4缓存层进程内 TTL memo,非 Redis单实例部署,无需跨进程共享;注释明确"swap for Redis if scaled out"src/lib/api/memo.ts:1
D5计数策略首页才算 total + 预聚合 stats 表仅无游标的首页返回 total;lang/category/sub 过滤命中预聚合表,避免深分页 countcatalog.service.ts:470、:482
D6下载任务立即标记 completed(D-6)无服务端转存,客户端直连 CDN 下载,task 存为 completed + completed_at=nowsrc/lib/services/downloads.service.ts:117
D7部署形态单机内网,非 k8s内网同事级并发,colima 单容器 + 单 Next 进程足够,省运维docs/SERVICE-OPS.md:9
D8鉴权固定 token 白名单,userId=token内网可信,paid- 前缀标记付费用户;不适合公网暴露auth.ts:48、DELIVERY.md:63

7. 与设计文档 (v4) 的落地对照表

维度图例:✅ 已落地 / 🔸 简化落地 / ❌ 未做。

7.1 已落地(✅)

v4 设计项落地证据
PostgreSQL 16 主存储 + tsvector/GIN 全文 + pg_trgmscripts/etl/finalize.sql:27(生成列 tsv)、:116(GIN tsv)、:117(trgm)
数据库三表 + 用户态表8 张表全部存在:podcasts/episodes/podcast_categories + playback_progress/download_tasks + 3 张 stats(schema.ts)
剧集 keyset 游标分页(by publishDate)catalog.service.ts:583
目录 offset / 稳定排序 keysetcatalog.service.ts:381
REST + 路径版本化 /v1实际为 /api/v1/podcasts/*(src/app/api/v1/...)
Zod 入口校验 / pino 结构化日志 / 禁 console.*shows/route.ts:25、logger.ts:1
Route Handlers 默认 Node runtime无 runtime='edge' 声明,全部 dynamic='force-dynamic'
离线 ETL + 脏行容错 + 批量后建索引finalize.sql:1、transform.mjs、docs/SERVICE-OPS.md:135
音频不代理,返回外部 CDN urlcatalog.service.ts:769(D-5)
分页页大小上限各 validations schema 限制 limit(src/lib/validations/podcasts.ts)

7.2 简化落地(🔸)

v4 设计项落地差异
Redis 7 + ioredis 热点缓存/限流改为进程内 Map + TTL memo;未做限流(memo.ts:9)
双层鉴权(API Key + JWT 轮换)改为固定 Bearer token 白名单,userId=token,无 JWT/refresh(auth.ts:36、env.ts:10)
k8s 多副本 + HPA + Ingress/TLS改为单机 colima 容器 + 单 Next 进程,HTTP 明文,无网关(docs/SERVICE-OPS.md:9)
node-postgres 连接池改用 postgres-js(client.ts:15),同为常驻连接池
版本(next 16.2.6 / drizzle 0.38 / zod 3.24 / pino 9)实际 next 16.2.7 / drizzle 0.45 / zod 4 / pino 10(package.json)

7.3 未做(❌)

v4 设计项状态
复用 Qt login token(SSO / JWKS / introspection,方案甲)未做。内网用固定 token;公网访问需走 v4 §7 JWT 方案(DELIVERY.md:63)
浏览器 SPA + CORS 白名单 + httpOnly refresh cookie + CSRF未做。客户端为 CEF 桌面端,未上浏览器 SPA
限流(按 Key + user_id,RateLimit-* 响应头)未做。代码中无限流中间件
自注册 / login / refresh / logout 鉴权端点未做。无 /v1/auth/* 路由
收藏 / 订阅(favorites / subscriptions 表与接口)未做。schema 仅有 playback_progress / download_tasks;用户态聚焦"播放进度 + 下载配额"
Hyperdrive / 外部托管 PG / 快照原子切换未做。单机自托管;重建走 reload.sh(约 20 分钟全量),非在线原子切换
独立搜索引擎(Meilisearch/Typesense)未做。停在 PG 全文检索(v4 推荐的起步阶段)

8. 已知边界与待确认

项说明锚点
鉴权仅适合内网UI 内置 dev-token,不适合公网暴露DELIVERY.md:63
服务器睡眠会断服需关闭"自动进入睡眠"或 caffeinate -s 配合 serve.shDELIVERY.md:64
音频/封面依赖公网来自外部 CDN,客户端需可出公网(或后续加代理端点)DELIVERY.md:65
公证状态DMG 已 Developer ID 签名,公证已配置(NOTARY_PROFILE=goland-notary);交付时标注"待公证"DELIVERY.md:10、:43
全量语言分布v4 标注"未核验、不臆断纯英文";本项目 language_stats 已按真实数据预聚合,可查deepresearch...v4.md:76、catalog.service.ts:252

9. 相关分册与文件索引

资源路径用途
架构决策真源deepresearch_podcast-catalog-api-architecture_20260604_v4.md框架无关契约 + k8s 设计推演
交付说明DELIVERY.md客户端分发、服务地址解析、公证、已知边界
运维手册docs/SERVICE-OPS.md启动/停止/排障、依赖链
API 设计podcast-api/docs/API-DESIGN.md端点契约 / 信封 / 错误码细节
DB Schemapodcast-api/src/lib/db/schema.ts8 张表 Drizzle 定义
ETLpodcast-api/scripts/etl/transform.mjs / finalize.sql / perf-stats.sql / reload.sh / run-etl.sh
服务配置podcast-api/.env.localDATABASE_URL、token 白名单
桌面端desktop/(README.md / CMakeLists.txt / src/)Qt6 + CEF 客户端
Web UIpodcast-api/public/app/(index.html / app.js / styles.css)桌面端加载的静态页面