@@ -0,0 +1,281 @@
|
||||
# 91短剧 · Next.js
|
||||
|
||||
Next.js App Router、React、TypeScript。保留 HTML 原稿布局和移动端导航,业务数据通过 Swagger API 动态获取。
|
||||
|
||||
## 运行
|
||||
|
||||
已使用 Node.js **22.17.1** 验证;最低要求 20.9。
|
||||
|
||||
```sh
|
||||
nvm use 22.17.1
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
开发、测试和生产启动的默认地址均为 http://localhost:21021,端口或环境变量修改后需要重启 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/*`,Next.js 服务端通过 `UPSTREAM_API_URL` 请求 Go 的 `/api/app/*`。开发、测试环境使用 `http://172.104.181.99:21022`,生产环境使用 `http://172.104.56.28:26125`。上游地址仅由服务端读取。默认应用端口均为 `21021`。公开接口缓存和 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_91dj_h5.tar.gz` 和 `web_91dj_h5.tar.gz.sha256`,上传相同文件及 `up_next_h5.sh`;同时刷新同名 `web_91dj_h5.md5` 供本地兼容校验。包内直接包含 `start.mjs`、`server.js`、`.next-test/`、`public/`、`config/test.env` 和 lockfile。测试包不带本机 `node_modules`,使用 `tar -xzf web_91dj_h5.tar.gz` 解压后,在 Linux 执行 `npm ci --omit=dev --no-audit --no-fund`,再执行 `node start.mjs`;默认端口为 21021。可通过 `DEPLOY_PACKAGE_NAME=自定义名称.tar.gz` 覆盖包名,校验文件随之生成。测试和生产使用同名压缩包,根目录文件对应最后一次执行的环境。
|
||||
|
||||
生产产物为 `web_91dj_h5.tar.gz` 和纯哈希格式的 `web_91dj_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 名使用 `91dj`:
|
||||
|
||||
| 环境 | SSH 目标 | 上传目录 | SCP / SSH 端口 |
|
||||
| --- | --- | --- | --- |
|
||||
| 测试 | `root@172.104.181.99` | `/server/web_91dj/h5` | `22889 / 22889` |
|
||||
| 生产 | `server@34.92.204.175` | `/home/server/web_91dj/h5` | `22889 / 22889` |
|
||||
|
||||
直接执行脚本会使用上述地址;仅打包继续使用 `--package-only`。需要其他目标时可覆盖:
|
||||
|
||||
```sh
|
||||
DEPLOY_HOST=user@test-host DEPLOY_DIR=/srv/91dj/h5 DEPLOY_RUN_DIR=/srv/91dj/h5 bash deploy_test.sh
|
||||
DEPLOY_JUMP_HOST=user@jump-host DEPLOY_JUMP_DIR=/srv/91dj/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_91dj/h5 && bash run.sh"`,由服务器现有的 `run.sh` 负责后续更新。上传目录和执行目录当前均为 `/server/web_91dj/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 程序为 `91dj_next_h5`,在服务器运行该脚本时可通过 `DEPLOY_PROGRAM` 覆盖;使用自定义包名时需设置对应的 `DEPLOY_PACKAGE_NAME`。服务器 `run.sh` 的解包、服务管理及包名规则以该脚本自身实现为准。
|
||||
|
||||
生产脚本将 `web_91dj_h5.tar.gz` 和 `web_91dj_h5.md5` 上传到 `server@34.92.204.175:/home/server/web_91dj/h5/`,SCP 与 SSH 默认端口均为 `22889`。上传成功后执行 `ssh -p 22889 server@34.92.204.175 "cd /home/server/web_91dj/h5 && bash rsupdate.sh"`。服务器现有的 `rsupdate.sh` 负责解包、保留生产私有配置和重启服务;本项目不上传或覆盖该脚本。上传失败不会触发远端更新,SSH 失败或返回失败状态会停止发布。可通过 `DEPLOY_JUMP_HOST`、`DEPLOY_JUMP_DIR`、`DEPLOY_SCP_PORT`、`DEPLOY_SSH_PORT` 覆盖目标,执行目录同步跟随上传目录。
|
||||
|
||||
生产 SSH 的远端命令文本与上述手动命令保持一致:通过路径校验后,`cd` 的目录不额外添加单引号,以兼容按原始命令文本匹配的 SSH 白名单。dry-run 与实际执行共用同一个命令,运行时会打印命令并保留远端日志。远端即使打印“发布成功”,只要此前输出含 `ERROR`(如 `curl` 报 HTTP 401)、`FATAL` 等失败状态,本地仍会报错;HTTP 401 的具体检查地址需核对服务器现有 `rsupdate.sh`,应用自身提供无需登录的 `/healthz`。
|
||||
|
||||
`npm run bundle:test` / `npm run bundle` 只整理已有构建;`npm run bundle:linux -- x64 glibc` 为生产产物准备 Linux 原生依赖。同一仓库的测试、生产脚本共用打包锁,压缩包及 `dist/` 已忽略提交。`/healthz` 是不触发游客登录、禁止缓存和索引的部署探针,不依赖上游接口。
|
||||
|
||||
## 页面路由
|
||||
|
||||
首页使用站点根地址,例如 `http://127.0.0.1:21021`,也支持 `http://127.0.0.1:21021/`;两者都直接显示首页。浏览器把纯域名的请求路径解析为 `/`,不需要跳转。短剧列表单独使用 `/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` 与最新地址字段。未配置或无效的链接不展示,不使用演示账号或首页链接兜底。
|
||||
- 个人中心 `/member/center/`,收藏、历史、点赞、已购和设置分别为 `/member/favorites/`、`/member/history/`、`/member/likes/`、`/member/purchased/`、`/member/settings/`。
|
||||
- 帮助与平台信息 `/info/:slug/`;充值 `/info/deposit/`;VIP `/info/deposit/vip/`。
|
||||
- 所有分页使用独立路径,例如 `/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` 保留;评论排序使用 `?commentSort=1`。切换排序时链接指向第一页,翻页时保留当前条件;第一页链接使用原页面路径,不执行自动跳转或地址替换。
|
||||
|
||||
页面直接根据 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-21 已按当前 91av Swagger 重新核对各页面,具体映射和接口缺项见 [页面接口对照](docs/api-contract.md)。短剧列表使用 `playlet/all/list`;专题采用顶层详情字段;首页未配置 `moreUrl` 的栏目使用首页真实数据兜底。按用户要求移除独立剧集入口及 `/series/`、`/video/:id/` 路由,旧地址直接返回 404。
|
||||
|
||||
页面 head 由本项目(`JHA-1047`)的 `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://91av-wz.80sygtm.com/swagger/index.html
|
||||
- `UPSTREAM_API_URL`:开发、测试环境为 `http://172.104.181.99:21022`,生产环境为 `http://172.104.56.28:26125`;兼容旧 `API_BASE_URL`。
|
||||
- `API_CHANNEL`:可选游客、登录和绑定的默认渠道码;已有首次渠道时优先保留。
|
||||
- 项目编号:`JHA-1047`。接口读取与业务处理位于 `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/*`,由 Next.js 按白名单代理到配置上游的 `/api/app/*`。Nginx 已有的 `/api/app/* → Go` 规则可以保留,其他 `/api/*` 交给 Next.js;不能将整个 `/api/` 都交给 Go。旧 Next.js `/api/app/*` 处理器仍保留供本地兼容访问。首页由服务端渲染,公开首页数据缓存 30 秒。公共接口不携带用户 token;用户接口使用 HttpOnly、SameSite Cookie。token 和密码不会写入浏览器本地存储或返回给前端脚本。
|
||||
|
||||
若显式配置为 `current`,则使用当前请求域名(默认配置不使用此模式)。该模式依赖 Nginx 将 `/api/app/*` 及 `/h5/*`、`/enkey`、`/newenkey` 等后端媒体路径转发到 Go,本站其他页面和 `/api/*` 由 Next.js 处理。上游请求若被再次转回 Next.js,会立即返回 `502` 和转发配置提示,避免递归请求。修改接口模式后重启服务即可,无须将当前域名写入前端构建产物。
|
||||
|
||||
请求处理参考 `jha-1045-hgdjnet-web`:Next.js 接口代理不根据 `Origin`、`Host` 或 `Sec-Fetch-Site` 拦截 POST,避免 CloudFront / Nginx 转发后域名或协议变化导致正常浏览器请求返回 403。浏览器继续请求当前站点的 `/api/*`,代理负责接口白名单、参数处理和会话传递,业务权限由上游校验。
|
||||
|
||||
## 登录与游客绑定
|
||||
|
||||
登录流程由本项目(`JHA-1047`)的 `src/lib/client/auth.ts` 和 `src/app/api/app/[...path]/route.ts` 实现,保留本站登录弹窗。
|
||||
|
||||
- 首次访问自动建立游客会话;已有 Cookie 时先读取 `auth/getInfo` 恢复身份、昵称和余额,接口未返回新 token 时继续使用原 token。
|
||||
- 游客、登录和绑定使用同一持久设备标识,发送 `devID` 和渠道;首次渠道取 `dc/ch/pc`,后续访问保持不变。绑定支持 `inviteCode/invite/code` 邀请参数。服务端按参考项目发送设备头,并透传有效的客户端 IP。
|
||||
- 绑定携带当前游客凭证以保留其数据;前端填写账号、密码和确认密码并同意协议,无需邮箱。上游要求的缺省邮箱由服务端生成为 `账号@91dj.local`;实际已绑定邮箱仍用于找回密码。
|
||||
- 绑定账号为 6–20 位字母或数字,密码为 8–64 位且包含字母和数字;登录兼容旧账号的 4 位密码及绑定允许的较长账号、密码,支持邮箱登录。
|
||||
- 登录失败保留原身份;成功后更新账户并重新读取当前页面。自动游客、登录和退出响应串行处理,旧请求的失效结果不会清除客户端刚登录的身份。
|
||||
- 游客请求明确返回会话失效时重建游客并重试一次;正式账号失效会清理会话并提示登录。登录、注册、找回密码自身的错误不触发游客恢复。
|
||||
- 退出清理会话,并跳过紧接着一次页面初始化的自动游客登录。当前页面保持未登录,主动登录或绑定可恢复使用。
|
||||
- 表单提交期间禁用重复提交;支持显示/隐藏密码,关闭和成功后均清除密码值。仅设备与渠道写本地存储,凭证保留在 HttpOnly Cookie。
|
||||
|
||||
## 已接入
|
||||
|
||||
- 首页轮播、公告、后台配置栏目、广告、题材及视频区块。
|
||||
- 短剧运营分类、剧集视频列表、榜单、专题、题材/标签、分页和更多列表。
|
||||
- 搜索、搜索联想、热搜、猜你喜欢;搜索记录单独保存在当前浏览器。
|
||||
- 短剧详情、章节列表、章节权限和实际播放地址;HLS 播放、媒体代理和播放上报。
|
||||
- 游客会话、登录、注册/游客绑定、找回密码、资料与头像更新。
|
||||
- 个人中心统计、收藏、历史、点赞、已购列表;点赞、收藏、评论、回复、评论点赞。
|
||||
- 金币/VIP 套餐、支付方式、订单创建、余额查询和逐集解锁。
|
||||
- 应用列表、下载配置、固定平台说明页、反馈提交。
|
||||
|
||||
首页“推荐 / 最新 / 热门 / 随机”筛选项保持移除。栏目排序、列表数据、总数与观看权限由接口决定,失败时显示错误或空状态。
|
||||
|
||||
首页动态栏目沿用 HTML 的模块间距(桌面 48px、移动端 32px)与卡片网格,首个栏目不再复用原稿图文混排区域的选择器。首页封面统一为 3:4,图片居中裁切,避免接口方图、竖图混排时卡片高度不齐,并在图片解密加载前预留封面高度。
|
||||
|
||||
全站以 `91短剧-2组 (3).html` 的页面结构和样式为基准:短剧、剧集、专题、题材/标签、更多列表和搜索页沿用原稿网格、标题、筛选栏和封面框;排行榜使用原稿数字角标、五列布局及窄屏断点,排名按接口分页顺序连续计算,榜单 tab 保持独立路由。搜索关键词只在已转义的标题中高亮。
|
||||
|
||||
专题列表与专题详情按 HTML 直接展示卡片和分页,不增加“最新专题 / 热门专题”或“编辑推荐 / 最新 / 最热”等分类选项。
|
||||
|
||||
搜索结果由本项目(`JHA-1047`)的 `src/lib/client/search.ts` 和 `src/lib/seo-server.ts` 实现:服务端准备会话后调用 `playlet/search`,传入 `keyword/pageNum/pageSize`,默认每页 25 条;展示接口总数、关键词和实际 `pageSize`,剧名优先使用 `playName`,不再本地筛选接口结果。无结果优先展示 `everyoneWatching`,缺少推荐时请求 `playlet/hot/list`;推荐不计入搜索数量。`/search/` 可直接进入,空关键词跳过搜索接口,翻页使用 `/search/{关键词}/page/{页码}/`,搜索框、热搜、搜索历史和联想词入口均直接生成关键词路径,不通过重定向转换。标题高亮沿用 HTML 的 NFKC、大小写和空白处理;无结果/失败状态使用原稿插图,接口错误保留重试入口,不伪装为零结果。
|
||||
|
||||
个人中心、收藏、历史、点赞与已购页共用原稿的头像/金币余额卡片、资料入口、书架导航与卡片列表;个人中心默认展示真实观看历史,分页链接进入历史页。设置页恢复头像编辑、昵称保存和账号安全卡片,资料、头像、密码仍通过原有接口保存;登录/绑定弹窗恢复原稿输入框、眼睛图标与按钮层级。页脚与桌面导航采用新版 HTML 的排版。
|
||||
|
||||
“关于我们、用户协议、隐私政策、常见问题、2257、DMCA”六页固定使用 `src/lib/info-pages.ts` 中的原 HTML 文案与结构,不再请求富文本接口,底部入口也保持固定。每次访问均由服务端输出正文、标题和对应样式标记,无须等待首页或富文本接口。保留分节间距、折叠问答、编号段落和 `mailto:` 邮箱链接,反馈链接直接使用 `/info/feedback/`。
|
||||
|
||||
“关于我们”的“联系我们”沿用 HTML 分节和段落样式:商务合作读取 `config.telegram`(兼容 `telegramAdd`),用户反馈读取 `config.tgGroup`,空值再取 `newAddressInfo`;不使用邮箱或在线客服地址代替 TG。服务时间等附加说明读取 `config/footer/info` 的 `footerInfo.contactList`。这些内容随正文一起由服务端输出,未配置的项目不展示。
|
||||
|
||||
反馈页恢复原稿的问题选项、环境/设备面板、描述计数、上传区域和提交区。实际提交通过 `feedback/add`,环境信息随描述保存;图片通过现有 `upload/image` 上传,支持最多 3 张、每张 5MB 以内的 JPG/PNG/WebP,不展示接口尚未支持的视频上传。上传失败不清空表单,提交时阻止重复写入,离开页面后隔离迟到的结果。
|
||||
|
||||
详情页结构按原 HTML 调整:封面与资料卡、热度图标和标签、播放/下载与互动操作栏、三行简介展开/收起、选集横向分段和金币角标、官方链接、评论及嵌套回复。移动端复用原稿的封面、四列操作栏与五列选集布局;无下载配置时只显示播放入口,无推荐数据时不留空侧栏。内容和章节 ID 继续由接口提供,评论分页保持独立路由。
|
||||
|
||||
支付和解锁均由用户在页面确认后发起;创建订单不会直接把本地余额改为“支付成功”,余额通过用户信息接口刷新。
|
||||
|
||||
充值页按 `91短剧-2组 (3).html#info/deposit` 展示余额与金币用途卡片、套餐面板、推荐角标、选中标记、独立支付按钮和交易明细。先选择接口套餐,再点击“立即支付”打开确认弹窗;价格、赠送金币与流水收支读取真实字段,不使用原稿的模拟金额和交易记录。桌面端四列、移动端三列套餐,交易明细继续使用独立分页路由。
|
||||
|
||||
充值确认弹窗按原 HTML 展示订单摘要、赠送/合计金币、金额徽标、支付渠道图标与选中圆点、“去支付”按钮和确认说明。金币数读取 `baseCoin/giftCoin/totalCoin`,价格按接口分单位换算;仅展示套餐配置的支付方式。切换样式按钮会同步实际下单参数,提交期间禁止重复下单。金币和 VIP 下单成功后自动在新标签页打开支付地址;原页保留“打开支付页面”供再次打开同一订单,以及“我已完成支付,刷新余额”。点击去支付时先预留标签页以避免异步弹窗拦截,下单失败或离开页面时关闭等待页,浏览器禁止新窗口时自动在当前页继续支付。创建订单后仍需用户完成真实支付。
|
||||
|
||||
详情和播放页点击金币集数,直接打开原 HTML 样式的解锁弹窗,读取所选章节的最新权限、余额和价格;价格优先使用 `costCoins`,其次 `singleCoins`。确认后提交该集的 `chapterId/playletId`,成功后刷新余额并进入对应播放页;余额不足进入充值,会员章节进入会员套餐。读取期间禁止支付,支付期间禁止重复提交或切换目标;关闭弹窗、切换集数或离开页面会隔离迟到的读取结果。已允许观看的章节仍使用真实播放链接。
|
||||
|
||||
App 下载入口(顶部、悬浮图标、首页底部、详情页和 `/navigation/`)统一校验配置。配置返回前隐藏入口;没有有效的 HTTP(S) 下载链接时不展示。电脑端直接点击链接下载,已移除悬浮二维码面板和触发脚本;移动端保留点击下载弹窗,二维码只在实际点击时生成。
|
||||
|
||||
图片解密由本项目(`JHA-1047`)的 `src/lib/client/images.ts` 实现:使用相同密钥对前 100 字节做 XOR,识别 JPEG、PNG、GIF、WebP,并兼容未加密图片。封面、轮播、题材、广告、应用图标、用户/评论头像和视频 poster 统一处理;接口原图放在 `data-src` / `data-poster`,完成解密后展示 Blob URL。本地素材和下载二维码保持直接加载。移动端并发 2、桌面端并发 4,首屏主图和弹窗优先;共享请求与缓存、视口懒加载、12 秒超时及占位图回退,页面离开时释放 Blob URL。
|
||||
|
||||
播放逻辑由本项目(`JHA-1047`)的 `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-1047`)的 `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:21021 npm run smoke:api
|
||||
```
|
||||
|
||||
若游客登录服务暂时超时,可仅验证公开内容:
|
||||
|
||||
```sh
|
||||
SMOKE_PUBLIC_ONLY=1 PREVIEW_URL=http://127.0.0.1:21021 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`:年龄确认、广告队列和会话进度,以及控制器与浏览器事件适配测试。
|
||||
|
||||
## 埋点
|
||||
|
||||
按照 [Web SDK v1.2.0 文档](https://doc2.adcenteryyds.cc/web/getting-started.html) 、[飞书埋点规范](https://djprsdgo4i4n.jp.larksuite.com/wiki/X8sjwbzs2ib01Ak2TWzj9Y94pFd)(已通过 Google Chrome 读取,包含 2026-10-17 更新)为本项目(`JHA-1047`)接入,官方 SDK 固定保存在 `public/scripts/web-sdk-v1.2.0.js`。应用 ID 默认 `1047`,可通过 `NEXT_PUBLIC_TRACKER_APP_ID` 配置,必须与当前站点密文和上游应用一致;初始化优先读取 `config/info.reportSdkConfig`,可选 `NEXT_PUBLIC_TRACKER_ENCRYPTED_CONFIG` 作为同项目的密文备用配置。缺少配置或 SDK 加载失败时,业务页面继续工作。
|
||||
|
||||
- SDK 自动采集生命周期、网络和会话;关闭自动 PV/点击,由接入层按真实路径上报,避免重复。支持首页、榜单、分页、专题、搜索、账户、信息页和独立导航页;登录、游客恢复和退出后同步 UID,渠道沿用首次归因。
|
||||
- 内容曝光以可见元素为准,关闭的弹窗、被模态弹窗遮挡的页面、后台标签页和轮播副本不计入;关闭弹窗或恢复标签页后重新观察尚未曝光的元素。短剧、专题和普通福利应用均有曝光、点击;内容无父级时 `parent_item_id` 使用 `0`,满足 SDK 的必填非空字符串校验。
|
||||
- 广告保留接口广告代码和广告位字段,上报 `adImpression/adClick`;页面广告、大图弹窗、宫格弹窗按实际展示位置分别去重,避免同一广告出现在宫格时被首页曝光记录吞掉。宫格仅统计滚动区域内可见的项目。两种弹窗另按参考项目协议上报 `advertising` 的 `show/click/close` 行为;只关闭实际曝光过的项目,同一展示只记一次关闭,关闭不算广告点击。福利应用有 `advitiseId` 时使用该广告 ID 上报,没有广告关联时按普通内容统计。
|
||||
- 搜索成功后上报关键词与实际总数,结果点击使用含页码的全局位置;未接推荐引擎的追踪字段保留空串。顶部、侧栏、独立入口页渠道和 App 下载入口记录导航点击。
|
||||
- 短剧使用 `dramaEvent/dramaStatusChange`,记录展示、播放、暂停、快进/快退、完成、分享、切集和成功解锁;独立视频使用 `videoEvent`。终止播放失败分别上报 `ep_failed/video_failed`(播放失败),自动恢复中的中间错误和预告失败不作为正片失败上报。短剧完播同时补报 `ep_play`,进度固定 100%,保留最后一段播放时长。播放时长按实际播放区间累计,排除缓冲和免费预告,进度为百分比,倍速乘以 100。
|
||||
- 所有同源业务 API 请求携带 SDK 原始公共字段 `X-SDK-Common`,保留 SDK 返回的完整公共字段,包括应用、渠道、UID、数值时间戳、会话、设备、指纹和版本等;注册、登录、充值及 VIP 下单都使用同一份字段格式。字段来自 `WebSDK.getCommonFields()`,不重新生成指纹或会话 ID,等待最多 1.5 秒。
|
||||
- 点赞、收藏、评论、购买由上游服务端上报,前端传入 `X-Tracker-Request-Id/X-Tracker-Event-Name/X-DC-Request-Context`,包含完整公共字段和目标剧集上下文。Next.js 代理校验应用 ID、事件对应接口和字段结构后透传;游客重试复用原始快照和请求 ID,不额外从客户端发送业务成功事件。
|
||||
|
||||
SDK 未初始化时最多缓存 1000 条事件,配置到达后按顺序补发;配置接口的首次请求不等待 SDK 加载,避免互相等待,首次 SDK 脚本失败后在取得配置时再尝试加载。SDK 返回字段校验失败、事件不在允许列表、地址不可用或异常时,仅触发脱敏的 `tracker:diagnostic` 事件,不向浏览器控制台输出。自带 SDK 仅移除了三处控制台警告,保留字段校验、HTTPS 地址限制和配置解密失败兜底;网络重试和持久队列由官方 SDK 负责。
|
||||
|
||||
埋点回归使用 SDK 替身及项目自带官方 SDK 的真实字段校验器;浏览器验证使用同一 SDK 的事件地址参数,将签名后的测试记录发送到本机接收端,不向真实统计后台发送测试事件。生产环境还需在广告中心为当前应用的 `reportSdkConfig` 配置业务域名反向代理地址(SDK 的 `batchUrls`),由部署层转发到文档指定的生产批量上报接口,不能让客户端直连生产上报域名。代理需保留请求体、真实来源 IP 和 SDK 签名;目前未变更线上域名或 Nginx 配置。
|
||||
|
||||
“我的点赞”入口位于头像菜单和个人中心书架导航,路由为 `/member/likes/`,分页使用 `/member/likes/page/N/`。服务端通过当前账号请求 `GET /api/app/playlet/like/list?pageNum=N&pageSize=24`,列表不进入公开缓存或搜索索引;空列表显示原 HTML 的空状态和发现入口,接口失败显示重试状态。
|
||||
Reference in New Issue
Block a user