Files
2026-11-06 00:13:27 +07:00

263 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 黄果短剧 · Next.js
项目编号 `JHA-1050`,应用及埋点 App ID `1050`,npm 包与 `/healthz` 应用标识均为 `jha-1050-jgdj-web`。品牌名称统一为「黄果短剧」。
部署包、默认目录与 Supervisor 名已统一使用 `jgdj`。服务器地址和端口沿用原配置;首次发布前需准备对应目录、远端更新脚本和服务配置,并核对 Go 上游及后台 `reportSdkConfig` 均属于应用 `1050`。仓库地址登记为新项目的目标路径,本次不修改 Git remote。
Next.js App Router、React、TypeScript。保留 HTML 原稿布局和移动端导航,业务数据通过 Swagger API 动态获取。
当前视觉基准为 `禁果短剧_2组.html`:采用粉色主题、桌面双层品牌页头、首页主推轮播与六张横版推荐、历史悬浮面板及统一页脚。移动端沿用参考稿的菜单、搜索、双列内容和抽屉导航。栏目、剧集、广告及观看历史继续读取真实接口,HTML 内的示例剧目、账号、金额和联系方式不作为业务配置导入。
## 运行
已使用 Node.js **22.17.1** 验证;最低要求 20.9。
```sh
nvm use 22.17.1
npm ci # 或:pnpm install
npm run lock:check
npm run dev
```
本地可用 **npm** 或 **pnpm** 安装依赖;发版与测试服安装以 `package-lock.json` + `npm ci` 为准。两份 lock(`package-lock.json` / `pnpm-lock.yaml`)中的运行时版本须一致,`npm run lock:check`(构建/打包前会自动执行)会校验已安装版本与两份 lock,避免出现 Next 小版本漂移导致 `/healthz` 500。变更依赖后请同时更新两份 lock(`npm install` 与 `pnpm install`)。
开发、测试和生产启动的默认地址均为 http://localhost:21030,端口或环境变量修改后需要重启 Next.js。
```sh
npm run typecheck
npm run build
npm test
npm start
```
现在需要运行 Next.js 服务以提供 API 代理与动态首页,不再导出静态 `out/`。旧的静态预览进程需要停止后用新命令启动。
## 环境配置
本项目使用 `config/*.env` 区分业务环境,启动脚本在运行 Next.js 前加载配置。
| 环境 | 配置文件 | 构建命令 | 启动命令 | 构建目录 |
| ---- | ------------------------ | -------------------- | --------------------------------------------- | ------------- |
| 开发 | `config/development.env` | `npm run build:dev` | `npm run dev`;构建后可用 `npm run start:dev` | `.next-dev/` |
| 测试 | `config/test.env` | `npm run build:test` | `npm run start:test` | `.next-test/` |
| 生产 | `config/production.env` | `npm run build` | `npm start` | `.next/` |
测试环境按生产模式构建、启动,使用 `APP_ENV=test` 和 `NODE_ENV=production`;`npm run dev:test` 则使用测试配置启动开发服务器。业务环境通过 `APP_ENV` 区分,不把测试部署误设为单元测试使用的 `NODE_ENV=test`。
浏览器始终访问当前站点的相对地址 `/api/app/*`,Next.js 服务端通过 `UPSTREAM_API_URL` 请求 Go 的 `/api/app/*`。开发、测试环境使用 `http://172.104.181.99:26792`,生产环境使用 `http://139.162.49.252:21040`(app;web `21041`、h5 `21042`,H5 CDN `d23iqbsywbvs3m.cloudfront.net`)。上游地址仅由服务端读取。开发与测试默认端口 `21030`,生产 H5 端口 `21042`。公开接口缓存和 SEO 快照按解析后的上游域名隔离。修改服务器配置时,在对应目录新增 `config/test.local.env` 或 `config/production.local.env`;这些本地文件已忽略提交。优先级为:进程环境变量 > 对应 `*.local.env` > 对应基础配置。开发环境兼容旧 `.env.local`,其优先级低于 `config/development.local.env`;测试和生产不从旧 `.env.local` 加载本站业务配置。
原有 `API_BASE_URL` 仍作为兼容别名。在同一配置层内 `UPSTREAM_API_URL` 优先;较高配置层的旧变量仍可覆盖较低层的新变量,启动后统一为同一个上游地址。上游地址仅在服务端读取,不写入浏览器代码。
例如需要测试服务同时运行在另一端口,可在 `config/test.local.env` 写入:
```dotenv
PORT=21022
```
也可以临时运行 `npm run start:test -- --port 21022`。不同环境使用各自的构建产物及 SEO 快照缓存;先执行对应环境的构建命令,再执行其启动命令。`UPSTREAM_API_URL` 等服务端配置在启动时读取,修改后重启即可;`NEXT_PUBLIC_*` 在构建时写入浏览器代码,修改后应重新构建。
开发环境默认关闭公共接口缓存,测试和生产保留原有缓存上限;可用 `PUBLIC_DATA_CACHE_TTL_MS` 覆盖。`npm run check` 会先完成生产构建(含类型检查),再运行完整本地回归测试。
## 打包与部署
使用 Node.js 22.17.1;生产跨平台打包需要 npm >=10.9.2。两份脚本均先构建、运行回归测试,再整理 Next.js standalone、静态资源和对应环境配置。
```sh
# 预览流程,不构建、不连接服务器
bash deploy_test.sh --dry-run
bash deploy_prod.sh --dry-run
# 只生成本地压缩包,不上传
bash deploy_test.sh --package-only
bash deploy_prod.sh --package-only
```
测试产物为根目录的 `web_jgdj_h5.tar.gz` 和 `web_jgdj_h5.tar.gz.sha256`,上传相同文件及 `up_next_h5.sh`;同时刷新同名 `web_jgdj_h5.md5` 供本地兼容校验。包内直接包含 `start.mjs`、`server.js`、`.next-test/`、`public/`、`config/test.env` 和 lockfile。测试包不带本机 `node_modules`,使用 `tar -xzf web_jgdj_h5.tar.gz` 解压后,在 Linux 执行 `npm ci --omit=dev --no-audit --no-fund`,再执行 `node start.mjs`;默认端口为 21030。可通过 `DEPLOY_PACKAGE_NAME=自定义名称.tar.gz` 覆盖包名,校验文件随之生成。测试和生产使用同名压缩包,根目录文件对应最后一次执行的环境。
生产产物为 `web_jgdj_h5.tar.gz` 和纯哈希格式的 `web_jgdj_h5.md5`,包含 `.next/` 和 standalone 裁剪依赖,默认适配 Linux x64/glibc,可直接执行 `node start.mjs`。可通过 `DEPLOY_TARGET_ARCH=arm64`、`DEPLOY_TARGET_LIBC=musl` 选择目标架构和 libc。打包时仅从 lockfile 安装对应 Linux Sharp/libvips,并校验 ELF 架构,不携带 macOS 原生模块。
两种包仅包含对应的基础 `config/*.env`,不打包 `.env*`、`*.local.env` 和 SEO 缓存。服务器的私有覆盖配置放在发布目录下对应的 `config/test.local.env` / `config/production.local.env`;修改 `NEXT_PUBLIC_*` 必须重新构建。本站媒体密钥由上游接口代理提供,无需额外的本地 `m3u8.key` 文件。
部署默认值由两份脚本开头的变量定义,并支持环境变量覆盖;当前上传配置如下。本项目包名和 Supervisor 名使用 `jgdj`:
| 环境 | SSH 目标 | 上传目录 | SCP / SSH 端口 |
| ---- | ---------------------- | -------------------------- | --------------- |
| 测试 | `root@172.104.181.99` | `/server/web_jgdj/h5` | `22889 / 22889` |
| 生产 | `server@34.92.204.175` | `/home/server/web_jgdj/h5` | `22889 / 22889` |
直接执行脚本会使用上述地址;仅打包继续使用 `--package-only`。需要其他目标时可覆盖:
```sh
DEPLOY_HOST=user@test-host DEPLOY_DIR=/srv/jgdj/h5 DEPLOY_RUN_DIR=/srv/jgdj/h5 bash deploy_test.sh
DEPLOY_JUMP_HOST=user@jump-host DEPLOY_JUMP_DIR=/srv/jgdj/h5 bash deploy_prod.sh
```
测试当前 SSH 端口为 22889,可通过 `DEPLOY_SSH_PORT` 修改;它与 `config/test.env` 中的应用监听端口 `PORT` 分别配置。`deploy_test.sh` 上传成功后执行 `ssh -p 22889 root@172.104.181.99 "cd /server/web_jgdj/h5 && bash run.sh"`,由服务器现有的 `run.sh` 负责后续更新。上传目录和执行目录当前均为 `/server/web_jgdj/h5`,分别可用 `DEPLOY_DIR`、`DEPLOY_RUN_DIR` 覆盖;脚本不上传或覆盖远端 `run.sh`。部署测试从脚本帮助信息读取当前默认执行目录,并单独验证目录覆盖及上传、执行失败时的退出行为,修改服务器目录无需同步修改测试断言。上传失败不会执行远端更新,远端命令失败会立即退出;`--package-only` 和 `--dry-run` 均不执行 SSH。可通过 `DEPLOY_CHECK_URL` 在远端命令成功后增加公网健康检查。
随包上传的 `up_next_h5.sh` 保留供服务器按需调用:该脚本校验 SHA-256、检查并解压 tar.gz、安装运行依赖、保留本地覆盖与 SEO 缓存,然后切换版本、启动服务并检查 `/healthz` 与首页,失败自动恢复旧版本。默认 Supervisor 程序为 `jgdj_next_h5`,在服务器运行该脚本时可通过 `DEPLOY_PROGRAM` 覆盖;使用自定义包名时需设置对应的 `DEPLOY_PACKAGE_NAME`。服务器 `run.sh` 的解包、服务管理及包名规则以该脚本自身实现为准。
生产脚本将 `web_jgdj_h5.tar.gz` 和 `web_jgdj_h5.md5` 上传到 `server@34.92.204.175:/home/server/web_jgdj/h5/`,SCP 与 SSH 默认端口均为 `22889`。上传成功后执行 `ssh -p 22889 server@34.92.204.175 "cd /home/server/web_jgdj/h5 && bash rsupdate.sh"`。服务器现有的 `rsupdate.sh` 负责解包、保留生产私有配置和重启服务;本项目不上传或覆盖该脚本。上传失败不会触发远端更新,SSH 失败或返回失败状态会停止发布。可通过 `DEPLOY_JUMP_HOST`、`DEPLOY_JUMP_DIR`、`DEPLOY_SCP_PORT`、`DEPLOY_SSH_PORT` 覆盖目标,执行目录同步跟随上传目录。
生产 SSH 命令按参考项目统一由 `REMOTE_UPDATE_COMMAND` 生成,预览与实际执行的命令文本一致,目录经过安全字符校验后不再额外添加单引号,兼容服务器按文本匹配的命令白名单。执行前打印远端命令,便于核对权限规则和自定义目录;完整输出继续保存在发布日志中。
`npm run bundle:test` / `npm run bundle` 只整理已有构建;`npm run bundle:linux -- x64 glibc` 为生产产物准备 Linux 原生依赖。同一仓库的测试、生产脚本共用打包锁,压缩包及 `dist/` 已忽略提交。`/healthz` 是不触发游客登录、禁止缓存和索引的部署探针,不依赖上游接口。
## 页面路由
首页使用站点根地址,例如 `http://127.0.0.1:21030`,也支持 `http://127.0.0.1:21030/`;两者都直接显示首页。浏览器把纯域名的请求路径解析为 `/`,不需要跳转。短剧列表单独使用 `/shorts/`。
其他页面统一使用以 `/` 结尾的路径路由,可直接打开、刷新、在新标签页打开,并使用浏览器前进和后退。链接生成时就包含尾斜杠,不通过跳转或地址替换补齐。
- 首页 `/`;短剧 `/shorts/`;专题 `/topics/`;热播榜 `/rank/hot/`、新剧榜 `/rank/new/`、追剧榜 `/rank/follow/`;应用 `/apps/`;回家的路 `/home-road/`。榜单 tab 使用页面链接,数据类型由路径决定;原 `/rank/` 仍可直接访问热播榜,不重定向。
- 官方入口 `/navigation/`:独立展示线路、官方渠道与 App 下载,不加载首页导航、播放器或账户交互;首页和业务页面不再弹出入口导航页。
- 专题、题材、标签、栏目:`/topics/:id/`、`/theme/:id/`、`/tag/:id/`、`/config/:id/`。
- 详情 `/detail/:id/`;章节播放 `/play/:id/:chapterId/`。
- 内容路由优先使用接口返回的英文或拼音 `slug`(字母、数字、连字符、下划线),中文或无效 slug 回退到 ID,例如 `/detail/city-story/`、`/play/city-story/90001/`、`/topics/urban/`、`/tag/urban/`、`/config/editors-choice/`。章节段仍是章节 ID。卡片、轮播、选集、评论/列表分页、解锁后跳转和 SEO 地址使用同一规则;原 ID 地址继续直接访问。有配置的 `moreUrl` 优先级不变。
- 搜索 `/search/关键词/`;分页 `/search/关键词/page/2/`。关键词通过 `encodeURIComponent` 作为单个路径段编码,支持中文和特殊字符。
- 独立导航页 `/navigation/` 从 `config/info` 读取站名、介绍、`navPageList`、永久入口、TG/X 和 App 下载地址。线路按 `line` 排序;旧配置兼容 `dhNewUrlAddr1/2/3` 与最新地址字段。未配置或无效的链接不展示,不使用演示账号或首页链接兜底。
- 帮助与平台信息 `/info/:slug/`(关于我们、用户协议、隐私政策、常见问题、2257、DMCA)。个人中心、收藏/历史/点赞/已购、账号设置、充值/VIP、意见反馈页面已移除,访问返回 404。
- 所有分页使用独立路径,例如 `/shorts/page/2/`、`/rank/new/page/2/`、`/topics/31/page/2/`、`/member/history/page/2/`、`/info/deposit/vip/page/2/`;详情和播放页的 `/page/2/` 对应评论第 2 页。页码、上一页和下一页均为真实链接,可直接访问、刷新和前进后退。
- 排序通过 `?sort=2` 保留。切换排序时链接指向第一页,翻页时保留当前条件。
页面直接根据 URL 路径加载,不解析或转换旧 hash 路由。站内导航、动态卡片、播放选集、搜索和会员入口均使用路径地址;无效路径返回 404。
页面地址统一使用尾斜杠。`src/proxy.ts` 对有效页面的无尾斜杠地址返回 `308` 永久重定向,例如 `/shorts` → `/shorts/`,分页、搜索关键词和查询参数完整保留;首页 `/` 直接响应。Next.js 保留 `trailingSlash: true` 和 `skipTrailingSlashRedirect: true`,由 Proxy 仅处理页面,API、播放列表、密钥、静态资源地址不受影响;无效路径直接返回 404。
## 接口配置
顶部、侧栏和移动导航由 `navigation/header.categoryList` 在服务端渲染,按接口顺序显示名称、地址和二级导航。URL 导航与二级导航显示在主导航,底部类型不混入;成功返回空列表时不注入固定菜单。`/hot` 直接生成本站 `/rank/hot/` 链接;已知系统入口的空地址按本站路由生成。导航公共数据缓存 60 秒,后台更新在缓存过期后的页面请求生效;页面高亮仍按当前路由判断。接口图标优先,缺失时沿用本站图标;外链添加 nofollow 等属性。
2026-10-26 已按 SGDJ Swagger 核对页面接口,具体映射见 [页面接口对照](docs/api-contract.md)。首页读取 `modules`,更多栏目使用 `playlet/home-config/list` 真实分页;短剧四种推荐使用 `playlet/operate/list`,题材使用 `playlet/all/list`。专题列表按区块显示,详情直接传 `topicSlug` 或 ID。此前移除的独立剧集入口及 `/series/`、`/video/:id/` 路由继续返回 404。
页面 head 由本项目(`JHA-1050`)的 `src/lib/seo-server.ts` 和 `src/lib/seo.ts` 生成:全站名称与首页 TDK 取 `config/info.seoInfo`,栏目 TDK 优先使用 `translation/category/list` 的导航配置,具体专题、剧集使用对应接口信息。兼容字符串形式的 SEO 配置及 `keyworld` 等旧字段。`/navigation/` 使用 `seoNavInfo`。
canonical 的域名优先取 `config.foreverUrlAddr`,其次取 `newAddressInfo.foreverUrlAddr`,只使用配置中的永久域名,不回退到请求 Host、API 或 CDN 域名。配置沿用 60 秒公共缓存,成功取得有效永久域名后,将公开 SEO 信息保存到 `.next/cache/seo/`,按 API 地址隔离;接口超时、报错、配置或永久域名暂缺时沿用最近一次有效值,服务重启也可读取已有快照。快照不包含 SDK 配置、账号或凭证;部署时需保留该缓存目录,完全没有有效配置/快照时不输出猜测的 canonical 和站点 JSON-LD。后台更新为有效永久域名后,缓存过期后的新页面请求采用新配置;同一浏览器文档沿用服务端给出的 canonical、site_name 和发布者身份。
canonical 不带查询参数,分页各自生成规范地址和有边界的 `prev/next`,第 1 页使用栏目地址。搜索、账户、充值和工具页按参考项目输出 `noindex`。服务端将 TDK、robots、Open Graph、Twitter 和图标信息直接写入 `<head>`,每次页面请求都返回对应页面的完整 head。本次未新增统计脚本。
`/robots.txt` 由 `src/app/robots.ts` 提供纯文本响应,不依赖账户会话或请求 Host。生产环境(`APP_ENV=production`)允许页面和 JS/CSS、图片、播放资源抓取,仅限制 `/api/` 和 `/healthz`;开发、测试环境禁止全站抓取。搜索、账户、充值、工具和重复页面继续使用页面级 `noindex`,保留爬虫读取这些标记的权限。生产环境声明 `Sitemap: https://永久域名/sitemap.xml`,域名与 canonical 共用配置及最近一次有效快照;完全没有有效域名时仅省略 Sitemap 行,抓取规则继续返回。开发、测试环境不声明 Sitemap。本项只添加声明,XML 文件仍由后续 sitemap 服务提供。
公开正文与 SEO 使用同一次服务端取数:短剧、三个榜单、专题列表/详情、题材/标签、首页更多栏目、短剧详情、播放页及各自分页直接输出当前页的标题、简介、内容卡片、集数和已有评论,分页与免费集数保留原生链接。搜索结果也直接输出首屏正文,继续保持 `noindex`。详情/播放和栏目模板在服务端与站内导航间共用,沿用现有 HTML 样式。
请求与渲染方式:服务端先完成当前页面取数,再返回页头和完整正文;福利应用、回家的路、反馈、个人中心、收藏/历史/点赞/已购、设置及充值/VIP 页面也直接输出内容。链接和搜索使用浏览器原生页面请求,旧页面保留到新响应到达,不清空正文或插入页面加载占位。浏览器复用首屏快照绑定轮播、播放、表单和支付等交互,不再重复获取整页数据;提交成功后重新请求当前页面。
公共配置、导航、分类、页脚和标签选项使用 60 秒进程内缓存,首页使用 30 秒;同一上游地址及查询参数的并发请求合并,返回独立数据副本,缓存有数量上限。配置及首页刷新失败可沿用已成功取得的数据;错误和无效响应不写入缓存。账户、余额、套餐及付费观看权限不使用公共缓存,HTML 继续保持私有响应。可用 `PUBLIC_DATA_CACHE_TTL_MS` 缩短公共缓存时间,设为 `0` 可关闭;多实例各自维护缓存。
详情、章节、选集、评论和推荐按 URL 中的 ID 并行请求。浏览器复用首屏中的公开配置和页脚数据,下载与广告交互不再等待重复取数;SDK 配置仍单独读取,不写入 HTML。支持浏览器前进后退缓存时,公开页保留 DOM、滚动位置和交互,并在后台恢复当前会话和页面埋点;账户、播放及打开弹窗的页面仍重新请求,防止恢复已处置的支付/权限状态。
有效业务页面在 Proxy 中恢复账号或建立游客会话,将同一身份的 Cookie 传给本次服务端渲染并写回浏览器。客户端复用已准备的用户信息,避免首屏再次初始化游客。正式账号过期显示登录入口,接口异常直接返回重试状态;账户页面保持 `private, no-store` 和 `noindex`。首屏账号、账户列表、套餐、流水和播放器数据均按字段白名单传递,未授权付费视频源、服务端账号凭证及 SDK 私密配置不写入 HTML;点击金币集数仍读取实时权限、价格并由用户确认解锁。当前 Next.js 动态参数在路由入口只解码一次,中文和含空格、斜杠、百分号的搜索词与浏览器保持一致。
JSON-LD 参考 `huangguo.com` 的品牌节点及参考项目 `seoser/jsonld.go`:收录页输出 `Organization`、`WebSite` 和对应 `WebPage` / `CollectionPage`,列表输出实际内容的 `ItemList`,详情使用 `TVSeries`,播放页关联 `TVEpisode` 与具备真实封面、发布时间的 `VideoObject`。关于页使用 `AboutPage`,常见问题直接从页面可见问答生成 `FAQPage`。URL、站名与分页序号沿用现有动态 SEO 配置;不使用参考站的品牌、域名、伪造日期或评分。每份页面响应包含本页的原生 JSON-LD script,不收录页不输出旧页面的数据。测试覆盖脚本安全转义和重复节点清理。
- 文档:https://sgdj-h5.80sygtm.com/swagger/index.html
- `UPSTREAM_API_URL`:开发、测试环境为 `http://172.104.181.99:26792`,生产环境为 `http://139.162.49.252:21040`;兼容旧 `API_BASE_URL`。
- `API_CHANNEL`:可选游客、登录和绑定的默认渠道码;已有首次渠道时优先保留。
- 项目编号:`JHA-1050`。接口读取与业务处理位于 `src/lib/api/server.ts`、`src/lib/client/config.ts`、`src/lib/client/channel.ts`、`src/lib/client/auth.ts` 和 `src/lib/client/account.ts`。
浏览器与服务端业务接口统一使用 `/api/app/*` 前缀。请求进入 Next.js 时,由 `src/app/api/app/[...path]/route.ts` 按白名单代理到配置上游的同名路径,并负责参数处理及 HttpOnly、SameSite 会话 Cookie;旧 `/api/*` 入口保留兼容。首页由服务端渲染,公开首页数据缓存 30 秒。公共接口不携带用户 token;Next.js 代理不向前端脚本返回 token,也不将 token 或密码写入浏览器本地存储。
部署时需核对 `/api/app/*` 的实际处理服务:若 Nginx 将该路径直接转发给 Go,请求会跳过 Next.js 的参数处理、Cookie 与 Authorization 转换及 token 过滤,登录、绑定、退出和受保护接口需按 Go 的鉴权约定单独验证。若该路径由 Next.js 处理,`UPSTREAM_API_URL` 应指向可独立访问的 Go 上游,避免请求再次回到本站。
若显式配置为 `current`,则使用当前请求域名(默认配置不使用此模式)。该模式依赖 Nginx 将上游的 `/api/app/*` 及 `/h5/*`、`/enkey`、`/newenkey` 等后端媒体路径转发到 Go,本站其他页面由 Next.js 处理;浏览器业务请求也使用 `/api/app/*`,需满足上一段的后端鉴权约定。上游请求若被再次转回 Next.js,会立即返回 `502` 和转发配置提示,避免递归请求。修改接口模式后重启服务即可,无须将当前域名写入前端构建产物。
POST 请求按参考项目转发,不在 Next.js 代理中比较 `Origin`、`Host` 或 `Sec-Fetch-Site`。CDN、反向代理修改内部域名或协议时,不再因来源不匹配提前返回 403;上游返回的鉴权及业务错误继续传回。接口白名单、请求方法限制、会话 Cookie 和代理循环检测继续生效。
请求头按 `jha-1045-hgdjnet-web` 的 `common/request.js` 与 `useragent/build.go` 对齐:浏览器业务请求使用 `Accept: application/json`、`credentials: include`,SDK 就绪后由其原始字段生成 `X-User-Agent`;JSON 设置 Content-Type,上传表单由运行时生成 boundary。Next.js 的 API、SSR 和媒体代理共用 `src/lib/api/client-headers.ts`,透传原生 User-Agent,优先保留客户端已有 X-User-Agent,缺失时解析 UA 中的设备、系统和浏览器信息,并读取 `sid` / `_s_token_` Cookie 补充 SID。移除固定的 `jgdj-web/1.0` 和 `server-public` 设备头;设备 ID 仍通过业务参数和 SDK 原始字段传递。
有效客户端 IP 优先取 X-Real-IP,其次取 X-Forwarded-For 首个地址,上游只发送 X-Real-IP。公开接口仍不附带账号凭证,私有接口使用当前会话 Authorization;签名媒体和密钥使用 `Accept: */*`,不转发业务 Cookie 或 Authorization。合法 SDK 公共字段保持原值,短剧服务端事件上下文继续按业务路由校验后透传。共享缓存只保存公开响应数据,不保存访客请求头。
## 游客会话
站点只保留游客会话:`src/lib/client/api.ts` 的 `bootstrapGuestSession` 在进入站点时调用 `auth/guest`(已有凭证时先软读取 `auth/getInfo`),`src/lib/api/page-session.ts` 与 `src/lib/seo-server.ts` 在服务端读取会话 Cookie。登录、注册、找回密码、资料修改、个人中心、金币充值、意见反馈与评论均已移除。
## 已接入
- 首页轮播、公告、后台配置栏目、广告、题材及视频区块。
- 短剧运营分类、剧集视频列表、榜单、专题、题材/标签、分页和更多列表。
- 搜索、搜索联想、热搜、猜你喜欢;搜索记录单独保存在当前浏览器。
- 短剧详情、章节列表、章节权限和实际播放地址;HLS 播放、媒体代理和播放上报。
- 游客会话;点赞、收藏、分享。
- 需付费的剧集不在网页解锁:播放区显示“本集请在 黄果短剧 App 观看”并提供 App 下载按钮,选集中以锁图标标出。
- 详情页与播放页底部“推荐视频”读取 `playlet/guess-like/list`(12 条,排除当前剧)。
- 应用列表、下载配置、固定平台说明页。全站唯一对外联系方式为商务合作 Telegram `@jbd125`(`src/lib/contact.ts`)。
首页“推荐 / 最新 / 热门 / 随机”筛选项保持移除。栏目排序、列表数据、总数与观看权限由接口决定,失败时显示错误或空状态。
首页动态栏目沿用 HTML 的模块间距(桌面 48px、移动端 32px)与卡片网格,首个栏目不再复用原稿图文混排区域的选择器。首页封面统一为 3:4,图片居中裁切,避免接口方图、竖图混排时卡片高度不齐,并在图片解密加载前预留封面高度。
全站以原始 HTML 设计稿的页面结构和样式为基准:短剧、剧集、专题、题材/标签、更多列表和搜索页沿用原稿网格、标题、筛选栏和封面框;排行榜使用原稿数字角标、五列布局及窄屏断点,排名按接口分页顺序连续计算,榜单 tab 保持独立路由。搜索关键词只在已转义的标题中高亮。
专题列表与专题详情按 HTML 直接展示卡片和分页,不增加“最新专题 / 热门专题”或“编辑推荐 / 最新 / 最热”等分类选项。
搜索结果由本项目(`JHA-1050`)的 `src/lib/client/search.ts` 和 `src/lib/seo-server.ts` 实现:服务端准备会话后调用 `playlet/search`,传入 `keyword/pageNum/pageSize`,默认每页 25 条;展示接口总数、关键词和实际 `pageSize`,剧名优先使用 `playName`,不再本地筛选接口结果。无结果优先展示 `everyoneWatching`,缺少推荐时请求 `playlet/operate/list?type=2`;推荐不计入搜索数量。`/search/` 可直接进入,空关键词跳过搜索接口,翻页使用 `/search/{关键词}/page/{页码}/`,搜索框、热搜、搜索历史和联想词入口均直接生成关键词路径,不通过重定向转换。标题高亮沿用 HTML 的 NFKC、大小写和空白处理;无结果/失败状态使用原稿插图,接口错误保留重试入口,不伪装为零结果。
“关于我们、用户协议、隐私政策、常见问题、2257、DMCA”六页固定使用 `src/lib/info-pages.ts` 中的原 HTML 文案与结构,不再请求富文本接口,底部入口也保持固定。每次访问均由服务端输出正文、标题和对应样式标记,无须等待首页或富文本接口。保留分节间距、折叠问答、编号段落和 `mailto:` 邮箱链接,反馈链接直接使用 `/info/feedback/`。
“关于我们”的“联系我们”沿用 HTML 分节和段落样式,只展示唯一的商务合作 Telegram `@jbd125`,不再读取后台邮箱、QQ、X 或交流群配置。
详情页使用全宽电影海报式横幅:右侧约 48% 为清晰海报并向左渐隐,左侧为分类标签、超大标题、“·”分隔的集数/热度/收藏/点赞、#标签、可展开简介与操作(立即观看、下载 App、点赞/收藏/分享)。播放页在播放器左右两侧悬浮上一集/下一集圆形按钮,播放器下方为描边大号集数标题、胶片式横向选集条和“推荐视频”区(`src/app/hg-media.css`)。卡片为 9:16 竖版封面,右上角“1/N”计数或“单集”,左下角方块角标,标题下方点线分隔与热度/题材。
App 下载入口(顶部、悬浮图标、首页底部、详情页和 `/navigation/`)统一校验配置。配置返回前隐藏入口;没有有效的 HTTP(S) 下载链接时不展示。电脑端直接点击链接下载,已移除悬浮二维码面板和触发脚本;移动端保留点击下载弹窗,二维码只在实际点击时生成。
图片解密由本项目(`JHA-1050`)的 `src/lib/client/images.ts` 实现:使用相同密钥对前 100 字节做 XOR,识别 JPEG、PNG、GIF、WebP,并兼容未加密图片。封面、轮播、题材、广告、应用图标、用户/评论头像和视频 poster 统一处理;接口原图放在 `data-src` / `data-poster`,完成解密后展示 Blob URL。本地素材和下载二维码保持直接加载。移动端并发 2、桌面端并发 4,首屏主图和弹窗优先;共享请求与缓存、视口懒加载、12 秒超时及占位图回退,页面离开时释放 Blob URL。
播放逻辑由本项目(`JHA-1050`)的 `src/lib/client/player.ts`、`src/lib/client/playback.ts` 和 `src/lib/api/media.ts` 实现:绝对地址直接播放,相对地址走 `/h5/m3u8/`;Safari 优先原生 HLS,其余浏览器优先 hls.js,无 MSE 时再尝试原生 HLS。保留移动端内联属性,HLS 致命错误最多恢复 3 次,重试会重建播放器;切换预告、正片或离开路由时销毁旧实例。锁定章节可看配置的免费预告,正片按 `canWatch` 和参考项目的旧字段兼容规则判定;金币解锁需用户确认,成功后重新获取账户与章节,预告不提交正片观看上报。上一集/下一集使用接口的真实章节 ID。
H.264/AVC 与 H.265/HEVC 使用 `hls.js` 1.7.3 完整版解析,保留 HEVC MPEG-2 TS 支持(参考项目为 1.6.13 完整版)。多编码清单按设备能力保留 H.264、`hvc1`、`hev1`,不强制改写编码或限制为 H.264。编码不兼容时在 hls.js 与原生 HLS 之间各尝试一次,保留签名与播放位置;普通网络/密钥错误仍走原有恢复逻辑。无可用解码方式时显示编码不支持提示。H.265 的实际解码依赖浏览器、系统及设备能力,前端不提供转码或软件解码器,参见 [hls.js 官方说明](https://github.com/video-dev/hls.js/blob/v1.7.3/README.md)。
媒体和 `/enkey`、`/newenkey` 使用 Route Handler 读取运行时 `UPSTREAM_API_URL`,保留完整签名查询参数,不转发应用登录 Cookie 或 Authorization。密钥上游先请求无尾斜杠路径,兼容不同服务的尾斜杠规则:只允许同一域名、同一密钥路径增加或删除尾斜杠的一次跳转,签名查询、IP 和 UA 保持不变,返回值必须是 16 字节 AES 密钥。媒体支持 Range,清单和密钥均不缓存,业务错误响应不会作为视频内容返回。
## 年龄确认与广告弹窗
弹窗逻辑由本项目(`JHA-1050`)的 `src/lib/client/startup.ts` 实现,保留本项目的弹窗样式。
- 年龄确认使用会话记录 `hg_age_ok`,当前标签页会话首次进入时展示;确认后刷新、切换业务页面不再重复展示。存储不可用时仍可正常确认并继续;退出行为与参考项目一致,打开 Google。
- 大图广告使用配置接口 `advitise.dialogList`,按接口顺序逐条展示;先通过年龄确认,再显示已加载完成的广告,配置晚于确认返回时也能继续展示。
- 点击关闭或按 Esc 进入下一条;点击广告在新标签页打开配置链接。全部关闭后,同一会话内不再展示相同广告队列;刷新时从当前广告恢复。广告队列签名优先使用 `advertiseCode`,队列变化后重新展示。
- 图片加载失败、解码失败或 12 秒超时会跳过当前广告;全部失败时不记为已完成,后续页面可以重试。
- 大图广告结束后展示 `advitise.nineGridList` 宫格弹窗,保留 HTML 的四列图标、名称、间距、模糊遮罩和底部关闭按钮;没有大图广告时,年龄确认后直接展示宫格。按配置顺序渲染,不补齐模拟数据,图片复用解密逻辑,失败项不展示;没有有效图片时不弹出。点击应用打开配置链接并关闭弹窗,关闭按钮和 Esc 均可关闭;`gridAdDismissed` 独立记录当前宫格配置,同一会话不重复展示,配置变化后可重新展示。
- 宫格弹窗最大高度为视口的 80%,预留底部关闭按钮空间,超出的广告在宫格区域内纵向滚动;每次展示从顶部开始。
`/navigation/` 保持独立入口页,不加载年龄确认和广告队列。
## 验证
`npm test` 使用独立本地模拟上游和生产构建验证代理与 SSR,不写入真实业务数据。
启动服务后可执行真实接口检查:
```sh
PREVIEW_URL=http://127.0.0.1:21030 npm run smoke:api
```
若游客登录服务暂时超时,可仅验证公开内容:
```sh
SMOKE_PUBLIC_ONLY=1 PREVIEW_URL=http://127.0.0.1:21030 npm run smoke:api
```
2026-10-21 的类型检查、生产构建和全部 258 项测试通过;真实接口检查通过游客会话、公开与个人只读接口及 29 个页面。页面检查确认账户、充值、搜索和工具页的首个响应包含完整正文。真实支付、付费解锁及浏览器视觉验收不在此次验证范围内,详情见 [验证记录](docs/verification.md)。
## 目录
- `src/app/api/app/[...path]/route.ts`:业务代理、会话 Cookie、白名单和上游错误处理。
- `src/app/[...path]/page.tsx` 与 `src/lib/routing.ts`:页面路径入口、校验与解析。
- `src/app/navigation/page.tsx`:独立入口导航页,服务端读取线路、渠道和下载配置。
- `src/lib/api/`:接口类型、响应兼容和服务端请求。
- `src/lib/seo-server.ts`:服务端页面取数、正文模板和 SEO。
- `src/lib/api/page-session.ts`:页面会话准备及同一次请求的 Cookie 传递。
- `src/lib/api/public-cache.ts`:有上限的公共数据缓存、过期刷新及并发合并。
- `src/lib/site-shared-data.ts`:首屏公开配置和页脚数据白名单。
- `src/lib/client/`:共享页面模板及轮播、播放、登录、支付、下载等浏览器交互。
- `public/scripts/site-runtime.js`:原稿布局、移动端菜单和播放器控件。
- `public/assets/`:本地品牌、图标和空状态素材。
- `tests/api-integration.test.mjs`:SSR、鉴权隔离和代理边界测试。
- `tests/routing.test.mjs`:路径解析、搜索关键词编码、hash 地址不转换与原生页面导航测试。
- `src/lib/client/startup.ts` 与 `tests/startup.test.mjs`:年龄确认、广告队列和会话进度,以及控制器与浏览器事件适配测试。