From 30f88d3a209944fea3d906c644a4f8f3675e887a Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 27 Jul 2026 11:28:27 +0800 Subject: [PATCH] fix(external-entry): preserve legacy private room links --- docs/external-entry/README.md | 4 +- docs/external-entry/links.md | 89 ++++++++++++------- src/app/external-entry/page.tsx | 1 + .../__tests__/external_entry.test.ts | 27 +++++- src/lib/navigation/external_entry.ts | 4 +- 5 files changed, 89 insertions(+), 36 deletions(-) diff --git a/docs/external-entry/README.md b/docs/external-entry/README.md index 40b092ad..2e42c2a7 100644 --- a/docs/external-entry/README.md +++ b/docs/external-entry/README.md @@ -19,7 +19,7 @@ https:///external-entry?<参数> | 参数 | 可选值或格式 | 用途 | | --- | --- | --- | -| `target` | `chat`、`tip`、`private-zone` | 指定进入的页面;不传或值无效时进入聊天页。 | +| `target` | `chat`、`tip`、`private-zone` | 指定进入的页面;不传或值无效时进入聊天页。历史值 `private-room` 兼容进入 `private-zone`,新链接不得继续使用。 | | `character` | `elio`、`maya`、`nayeli` | 指定角色;不传或值无效时使用 Elio。 | | `psid` | string | 传入 Facebook Page-scoped User ID。 | | `mode` | `promotion` | 开启聊天促销模式。 | @@ -27,6 +27,8 @@ https:///external-entry?<参数> `mode=promotion` 只有在 `target=chat` 且同时提供有效的 `promotion_type` 时生效。 +标准私密空间参数是 `target=private-zone`。前端仅为已经发布的旧链接保留 `target=private-room` 兼容;`private_zone` 等其他拼写仍按无效目标处理并回退聊天页。 + ## PSID 与登录状态流程 PSID 保存、登录状态判断和 Facebook Identity 绑定逻辑见 [PSID 与登录状态流程图](./psid-login-flow.md)。 diff --git a/docs/external-entry/links.md b/docs/external-entry/links.md index af002af6..54f3f8a4 100644 --- a/docs/external-entry/links.md +++ b/docs/external-entry/links.md @@ -1,43 +1,66 @@ # 外部入口链接清单 -[返回外部入口接入说明](./README.md) +这份清单可以独立发送和直接点击。所有新链接统一使用标准参数 `target=private-zone`;历史参数 `target=private-room` 仅用于兼容已经发出的旧链接。 -以下链接可以直接点击打开。PSID 示例统一使用 `27511427698460020`。 +## 使用规则 + +- `target=chat`:进入聊天页。 +- `target=tip`:进入对应角色的打赏页。 +- `target=private-zone`:进入对应角色的私密空间。 +- `character` 可使用 `elio`、`maya`、`nayeli`;不传或值无效时使用 Elio。 +- `mode=promotion` 只在 `target=chat` 且 `promotion_type` 为 `voice`、`image` 或 `private` 时生效。 +- PSID 示例统一使用 `27511427698460020`;不要在入口中放登录 Token、Page Access Token 或 App Secret。 ## 测试环境 -| 入口 | 链接 | -| --- | --- | -| 普通聊天 | [打开普通聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat) | -| Maya 聊天 | [打开 Maya 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=maya) | -| Nayeli 聊天 | [打开 Nayeli 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=nayeli) | -| 携带 PSID 的聊天 | [打开 PSID 聊天示例](https://frontend-test.banlv-ai.com/external-entry?target=chat&psid=27511427698460020) | -| 语音促销 | [打开语音促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=voice) | -| 图片促销 | [打开图片促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=image) | -| 私密文本促销 | [打开私密文本促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=private) | -| 咖啡打赏 | [打开咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip) | -| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=maya) | -| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=nayeli) | -| 私密空间 | [打开私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone) | -| Maya 私密空间 | [打开 Maya 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=maya) | -| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=nayeli) | +| 入口 | 可点击链接 | 预期落地页面 | +| --- | --- | --- | +| 普通聊天(Elio) | [打开普通聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat) | `/characters/elio/chat` | +| Maya 聊天 | [打开 Maya 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=maya) | `/characters/maya/chat` | +| Nayeli 聊天 | [打开 Nayeli 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=nayeli) | `/characters/nayeli/chat` | +| 携带 PSID 的 Elio 聊天 | [打开 PSID 聊天示例](https://frontend-test.banlv-ai.com/external-entry?target=chat&psid=27511427698460020) | `/characters/elio/chat` | +| 语音促销 | [打开语音促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=voice) | `/characters/elio/chat`,保存语音促销意图 | +| 图片促销 | [打开图片促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=image) | `/characters/elio/chat`,保存图片促销意图 | +| 私密文本促销 | [打开私密文本促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=private) | `/characters/elio/chat`,保存私密文本促销意图 | +| Elio 咖啡打赏 | [打开 Elio 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=elio) | `/characters/elio/tip` | +| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=maya) | `/characters/maya/tip` | +| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=nayeli) | `/characters/nayeli/tip` | +| Elio 私密空间 | [打开 Elio 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=elio) | `/characters/elio/private-zone` | +| Maya 私密空间 | [打开 Maya 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=maya) | `/characters/maya/private-zone` | +| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=nayeli) | `/characters/nayeli/private-zone` | + +## 旧链接兼容验证 + +以下链接仅用于验证已经发出的 `private-room` 旧链接仍能正确进入私密空间。新投放链接不要再使用这些地址。 + +| 旧入口 | 可点击链接 | 预期落地页面 | +| --- | --- | --- | +| Elio 旧私密空间 | [验证 Elio 旧链接](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=elio) | `/characters/elio/private-zone` | +| Maya 旧私密空间 | [验证 Maya 旧链接](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=maya) | `/characters/maya/private-zone` | +| Nayeli 旧私密空间 | [验证 Nayeli 旧链接](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=nayeli) | `/characters/nayeli/private-zone` | ## 正式环境 -| 入口 | 链接 | -| --- | --- | -| 普通聊天 | [打开普通聊天](https://cozsweet.com/external-entry?target=chat) | -| Maya 聊天 | [打开 Maya 聊天](https://cozsweet.com/external-entry?target=chat&character=maya) | -| Nayeli 聊天 | [打开 Nayeli 聊天](https://cozsweet.com/external-entry?target=chat&character=nayeli) | -| 携带 PSID 的聊天 | [打开 PSID 聊天示例](https://cozsweet.com/external-entry?target=chat&psid=27511427698460020) | -| 语音促销 | [打开语音促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=voice) | -| 图片促销 | [打开图片促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=image) | -| 私密文本促销 | [打开私密文本促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=private) | -| 咖啡打赏 | [打开咖啡打赏](https://cozsweet.com/external-entry?target=tip) | -| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=maya) | -| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=nayeli) | -| 私密空间 | [打开私密空间](https://cozsweet.com/external-entry?target=private-zone) | -| Maya 私密空间 | [打开 Maya 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=maya) | -| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=nayeli) | +正式环境仅列标准链接,不再传播 `private-room` 旧参数。 -如需在其他入口携带 PSID,在链接末尾追加 `&psid=27511427698460020`。 +| 入口 | 可点击链接 | 预期落地页面 | +| --- | --- | --- | +| 普通聊天(Elio) | [打开普通聊天](https://cozsweet.com/external-entry?target=chat) | `/characters/elio/chat` | +| Maya 聊天 | [打开 Maya 聊天](https://cozsweet.com/external-entry?target=chat&character=maya) | `/characters/maya/chat` | +| Nayeli 聊天 | [打开 Nayeli 聊天](https://cozsweet.com/external-entry?target=chat&character=nayeli) | `/characters/nayeli/chat` | +| 携带 PSID 的 Elio 聊天 | [打开 PSID 聊天示例](https://cozsweet.com/external-entry?target=chat&psid=27511427698460020) | `/characters/elio/chat` | +| 语音促销 | [打开语音促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=voice) | `/characters/elio/chat`,保存语音促销意图 | +| 图片促销 | [打开图片促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=image) | `/characters/elio/chat`,保存图片促销意图 | +| 私密文本促销 | [打开私密文本促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=private) | `/characters/elio/chat`,保存私密文本促销意图 | +| Elio 咖啡打赏 | [打开 Elio 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=elio) | `/characters/elio/tip` | +| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=maya) | `/characters/maya/tip` | +| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=nayeli) | `/characters/nayeli/tip` | +| Elio 私密空间 | [打开 Elio 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=elio) | `/characters/elio/private-zone` | +| Maya 私密空间 | [打开 Maya 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=maya) | `/characters/maya/private-zone` | +| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=nayeli) | `/characters/nayeli/private-zone` | + +## 验收判断 + +打开入口后,浏览器地址最终应与“预期落地页面”一致。普通聊天和私密空间必须分别落在 `/chat` 与 `/private-zone`,不能再进入同一个页面。 + +如需在其他入口携带 PSID,在已有查询参数的链接末尾追加 `&psid=27511427698460020`。 diff --git a/src/app/external-entry/page.tsx b/src/app/external-entry/page.tsx index 4cd3714a..5303631c 100644 --- a/src/app/external-entry/page.tsx +++ b/src/app/external-entry/page.tsx @@ -6,6 +6,7 @@ * `/external-entry?target=chat&mode=promotion&promotion_type=voice` * `/external-entry?target=chat&character=maya` * `/external-entry?target=tip` + * `/external-entry?target=private-zone&character=nayeli` * * 页面不直接 `redirect()`,而是把数据交给 Client 组件先写入本地存储, * 再通过 `router.replace()` 清理 URL 并跳转到最终页面。 diff --git a/src/lib/navigation/__tests__/external_entry.test.ts b/src/lib/navigation/__tests__/external_entry.test.ts index 59ec7a27..d7f7f72c 100644 --- a/src/lib/navigation/__tests__/external_entry.test.ts +++ b/src/lib/navigation/__tests__/external_entry.test.ts @@ -14,12 +14,15 @@ describe("external entry navigation", () => { expect(resolveExternalEntryTarget({})).toBe(ROUTES.chat); }); - it("resolves only canonical targets", () => { + it("resolves canonical targets and the published private-room alias", () => { expect(resolveExternalEntryTarget({ target: "chat" })).toBe(ROUTES.chat); expect(resolveExternalEntryTarget({ target: "tip" })).toBe(ROUTES.tip); expect(resolveExternalEntryTarget({ target: "private-zone" })).toBe( ROUTES.privateZone, ); + expect(resolveExternalEntryTarget({ target: "private-room" })).toBe( + ROUTES.privateZone, + ); }); it("rejects removed aliases and unsupported targets", () => { @@ -49,6 +52,28 @@ describe("external entry navigation", () => { }), ).toBe(getCharacterRoutes("elio").privateZone); }); + + it.each([ + ["chat", "elio", getCharacterRoutes("elio").chat], + ["chat", "maya", getCharacterRoutes("maya").chat], + ["chat", "nayeli", getCharacterRoutes("nayeli").chat], + ["tip", "elio", getCharacterRoutes("elio").tip], + ["tip", "maya", getCharacterRoutes("maya").tip], + ["tip", "nayeli", getCharacterRoutes("nayeli").tip], + ["private-zone", "elio", getCharacterRoutes("elio").privateZone], + ["private-zone", "maya", getCharacterRoutes("maya").privateZone], + ["private-zone", "nayeli", getCharacterRoutes("nayeli").privateZone], + ["private-room", "elio", getCharacterRoutes("elio").privateZone], + ["private-room", "maya", getCharacterRoutes("maya").privateZone], + ["private-room", "nayeli", getCharacterRoutes("nayeli").privateZone], + ])( + "routes target=%s character=%s to %s", + (target, character, expectedDestination) => { + expect(resolveExternalEntryDestination({ target, character })).toBe( + expectedDestination, + ); + }, + ); }); describe("external entry facebook identity binding", () => { diff --git a/src/lib/navigation/external_entry.ts b/src/lib/navigation/external_entry.ts index 14cff3ef..ce562ea8 100644 --- a/src/lib/navigation/external_entry.ts +++ b/src/lib/navigation/external_entry.ts @@ -129,7 +129,9 @@ function resolveTarget( if (target === "chat") return ROUTES.chat; if (target === "tip") return ROUTES.tip; - if (target === "private-zone") return ROUTES.privateZone; + if (target === "private-zone" || target === "private-room") { + return ROUTES.privateZone; + } return null; }