refactor: 收口公开码搜索映射
This commit is contained in:
@@ -16,6 +16,14 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-04 Platform Public Code Search matcher / DTO 收口
|
||||||
|
|
||||||
|
- 背景:`resolvePlatformPublicCodeSearchPlan(...)` 已收口公开搜索顺序,但 `PlatformEntryFlowShellImpl.tsx` 仍内联 RPG by-code DTO 构造,以及拼图、大鱼吃小鱼、跳一跳、敲木鱼、宝贝识物、抓大鹅、方洞挑战、视觉小说和汪汪声浪的 `isSame*PublicWorkCode` 匹配、公开可见性过滤与详情卡映射。
|
||||||
|
- 决策:扩展 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,以 `mapRpgPublicCodeSearchDetailToGalleryCard(...)` 和各 `resolve*PublicCodeSearchMatch(...)` 收口 per-play 公开码匹配与 DTO 映射;壳层只保留 gallery 刷新、详情打开、Bark Battle runtime 特例、用户查询和错误归航副作用。`M3D-*` 旧抓大鹅前缀在 `isSameMatch3DPublicWorkCode(...)` 中继续匹配。
|
||||||
|
- 影响范围:平台首页搜索框、初始 `publicWorkCode` 恢复、各玩法公开作品号命中、RPG 公开作品 by-code 详情映射、Bark Battle runtime 内搜索启动。
|
||||||
|
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts src/services/publicWorkCode.test.ts`、针对搜索 Module / 壳层 / publicWorkCode 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||||
|
|
||||||
## 2026-06-04 Draft Generation Shelf 草稿打开 intent 收口
|
## 2026-06-04 Draft Generation Shelf 草稿打开 intent 收口
|
||||||
|
|
||||||
- 背景:`openPuzzleDraft` / `openMatch3DDraft` 在平台壳内重复判断已发布作品、缺 session、ready 未读、失败 notice、active / background 生成中、持久化 generating 和普通草稿恢复,导致壳层继续理解拼图稳定 ID、抓大鹅 notice key 与生成状态优先级。
|
- 背景:`openPuzzleDraft` / `openMatch3DDraft` 在平台壳内重复判断已发布作品、缺 session、ready 未读、失败 notice、active / background 生成中、持久化 generating 和普通草稿恢复,导致壳层继续理解拼图稳定 ID、抓大鹅 notice key 与生成状态优先级。
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
创作入口点击的占位、隐藏模板拦截、未知入口 no-op 与工作台启动目标收口到 `src/components/platform-entry/platformCreationLaunchModel.ts`,壳层只执行启动前准备、错误提示和受保护动作,规则见 [【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md)。
|
创作入口点击的占位、隐藏模板拦截、未知入口 no-op 与工作台启动目标收口到 `src/components/platform-entry/platformCreationLaunchModel.ts`,壳层只执行启动前准备、错误提示和受保护动作,规则见 [【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
平台入口公开码搜索的用户 ID、陶泥号、RPG 作品号、各玩法作品号前缀和失败回退顺序收口到 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,壳层只按计划执行网络读取、详情打开和错误归航副作用,规则见 [【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md)。
|
平台入口公开码搜索的用户 ID、陶泥号、RPG 作品号、各玩法作品号前缀、per-play 公开码匹配、详情卡 DTO 映射和失败回退顺序收口到 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,壳层只按计划执行网络读取、详情打开和错误归航副作用,规则见 [【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
个人“玩过作品”面板的玩法别名、`worldKey` 前缀兜底、RPG 公开详情 payload 和大鱼吃小鱼 gallery miss fallback 收口到 `src/components/platform-entry/platformPlayedWorkOpenModel.ts`,壳层只执行面板关闭、gallery 读取、详情打开和错误提示副作用,规则见 [【前端架构】PlatformPlayedWorkOpenModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPlayedWorkOpenModel收口计划-2026-06-04.md)。
|
个人“玩过作品”面板的玩法别名、`worldKey` 前缀兜底、RPG 公开详情 payload 和大鱼吃小鱼 gallery miss fallback 收口到 `src/components/platform-entry/platformPlayedWorkOpenModel.ts`,壳层只执行面板关闭、gallery 读取、详情打开和错误提示副作用,规则见 [【前端架构】PlatformPlayedWorkOpenModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPlayedWorkOpenModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -12,26 +12,29 @@
|
|||||||
|
|
||||||
- `resolvePlatformPublicCodeSearchPlan(keyword)`:输入用户搜索词,输出 `{ normalizedKeyword, steps }`;空输入返回 `null`。
|
- `resolvePlatformPublicCodeSearchPlan(keyword)`:输入用户搜索词,输出 `{ normalizedKeyword, steps }`;空输入返回 `null`。
|
||||||
- `PlatformPublicCodeSearchStep`:枚举壳层可执行的查找步骤,包括 `user-id`、`public-user-code`、`rpg-work`、各玩法公开作品步骤与 `bark-battle-work`。
|
- `PlatformPublicCodeSearchStep`:枚举壳层可执行的查找步骤,包括 `user-id`、`public-user-code`、`rpg-work`、各玩法公开作品步骤与 `bark-battle-work`。
|
||||||
|
- `mapRpgPublicCodeSearchDetailToGalleryCard(entry)`:把 RPG by-code 详情响应映射为公开作品卡,收口 `playCount` / `remixCount` / `likeCount` 的 `0` 兜底。
|
||||||
|
- `resolve*PublicCodeSearchMatch(entries, keyword)`:统一各玩法公开作品列表的公开码匹配、公开可见性过滤和详情卡 DTO 映射;拼图、大鱼吃小鱼、跳一跳、敲木鱼、宝贝识物、抓大鹅、方洞挑战、视觉小说和汪汪声浪都走此接口。
|
||||||
|
|
||||||
`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它保留 `getPublicAuthUserByCode`、各玩法 gallery 刷新 / 详情打开、Bark Battle runtime 特例和 missing work 归航副作用,只按 `steps` 顺序执行,前一步失败才尝试下一步。
|
`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它保留 `getPublicAuthUserByCode`、各玩法 gallery 刷新 / 详情打开、Bark Battle runtime 特例和 missing work 归航副作用,只按 `steps` 顺序执行,前一步失败才尝试下一步;壳层不再重复维护 per-play `isSame*PublicWorkCode` 匹配和 DTO 映射。
|
||||||
|
|
||||||
## Interface 约束
|
## Interface 约束
|
||||||
|
|
||||||
- 空白搜索词返回 `null`,壳层不得进入搜索 loading。
|
- 空白搜索词返回 `null`,壳层不得进入搜索 loading。
|
||||||
- `user_` / `user-` 开头的内部用户 ID 只执行 `user-id`,不回退作品号。
|
- `user_` / `user-` 开头的内部用户 ID 只执行 `user-id`,不回退作品号。
|
||||||
- `PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀只进入对应玩法公开作品查找;`M3D-*` 继续归入 `M3` / 抓大鹅。
|
- `PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀只进入对应玩法公开作品查找;`M3D-*` 继续归入并匹配 `M3` / 抓大鹅。
|
||||||
- `CW` 与 `1-8` 位纯数字先查 RPG 公开作品,再回退陶泥号。
|
- `CW` 与 `1-8` 位纯数字先查 RPG 公开作品,再回退陶泥号。
|
||||||
- 普通关键词与 `SY` 陶泥号保持既有顺序:先查陶泥号,再查 RPG 公开作品,再查汪汪声浪作品,最后再以陶泥号兜底。
|
- 普通关键词与 `SY` 陶泥号保持既有顺序:先查陶泥号,再查 RPG 公开作品,再查汪汪声浪作品,最后再以陶泥号兜底。
|
||||||
|
|
||||||
## Depth / Leverage / Locality
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
- **Depth**:壳层只消费短小的 `steps` Interface,搜索前缀、优先级和回退顺序藏入 Module Implementation。
|
- **Depth**:壳层只消费短小的 `steps` 与 match result Interface,搜索前缀、优先级、回退顺序、per-play 匹配和 DTO 映射藏入 Module Implementation。
|
||||||
- **Leverage**:新增公开作品前缀时,先扩展 Module 的 step union、前缀表和单测,再在壳层 Adapter 绑定对应执行函数。
|
- **Leverage**:新增公开作品前缀时,先扩展 Module 的 step union、前缀表、matcher 和单测,再在壳层 Adapter 绑定对应网络读取与打开动作。
|
||||||
- **Locality**:搜索计划规则集中在一个纯 Module;UI、网络、详情打开与 runtime 启动副作用继续留在壳层,避免把副作用 setter 变成浅 Interface。
|
- **Locality**:搜索计划与作品命中规则集中在一个纯 Module;UI、网络、详情打开与 runtime 启动副作用继续留在壳层,避免把副作用 setter 变成浅 Interface。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
- `npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts`
|
- `npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts`
|
||||||
|
- `npm run test -- src/services/publicWorkCode.test.ts`
|
||||||
- `npx eslint src/components/platform-entry/platformPublicCodeSearchModel.ts src/components/platform-entry/platformPublicCodeSearchModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet`
|
- `npx eslint src/components/platform-entry/platformPublicCodeSearchModel.ts src/components/platform-entry/platformPublicCodeSearchModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet`
|
||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run check:encoding`
|
- `npm run check:encoding`
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts`
|
|||||||
|
|
||||||
发现页 / 推荐页公开作品卡的作者行只显示可读公开昵称;不得把手机号掩码、`SY-*` 陶泥号或作品号拼接进卡片作者名。陶泥号搜索、作品号复制和完整作品身份只在搜索、详情页或明确的复制入口展示,避免卡片列表暴露账号标识。
|
发现页 / 推荐页公开作品卡的作者行只显示可读公开昵称;不得把手机号掩码、`SY-*` 陶泥号或作品号拼接进卡片作者名。陶泥号搜索、作品号复制和完整作品身份只在搜索、详情页或明确的复制入口展示,避免卡片列表暴露账号标识。
|
||||||
|
|
||||||
平台公开搜索的分流顺序统一由 `platformPublicCodeSearchModel.ts` 判定:`user_` / `user-` 内部用户 ID 只查用户 ID;`PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀分别直达对应玩法公开作品;`CW` 与 1-8 位纯数字先查 RPG 公开作品再回退陶泥号;普通关键词和 `SY` 陶泥号保持先查陶泥号、再查 RPG 作品、再查汪汪声浪作品、最后陶泥号兜底的既有顺序。平台壳只按计划执行网络读取、详情打开、Bark Battle runtime 特例和缺失作品归航,不在壳层重复维护前缀布尔链。
|
平台公开搜索的分流顺序、per-play 公开码匹配、公开可见性过滤和详情卡 DTO 映射统一由 `platformPublicCodeSearchModel.ts` 判定:`user_` / `user-` 内部用户 ID 只查用户 ID;`PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀分别直达对应玩法公开作品;`M3D-*` 作为抓大鹅旧前缀继续匹配;`CW` 与 1-8 位纯数字先查 RPG 公开作品再回退陶泥号;普通关键词和 `SY` 陶泥号保持先查陶泥号、再查 RPG 作品、再查汪汪声浪作品、最后陶泥号兜底的既有顺序。平台壳只按计划执行网络读取、详情打开、Bark Battle runtime 特例和缺失作品归航,不在壳层重复维护前缀布尔链、`isSame*PublicWorkCode` 或 DTO 映射。
|
||||||
|
|
||||||
个人“玩过作品”面板点击作品时,玩法别名、`worldKey` 前缀兜底、RPG 公开详情 payload 和大鱼吃小鱼缺 gallery 命中时的 fallback work 统一由 `platformPlayedWorkOpenModel.ts` 判定。平台壳只负责关闭面板、调用对应公开详情打开函数、刷新大鱼 gallery、优先使用真实 gallery 命中项和写入错误提示;不要在壳层重新维护 `worldType` / `worldKey` 分支链。
|
个人“玩过作品”面板点击作品时,玩法别名、`worldKey` 前缀兜底、RPG 公开详情 payload 和大鱼吃小鱼缺 gallery 命中时的 fallback work 统一由 `platformPlayedWorkOpenModel.ts` 判定。平台壳只负责关闭面板、调用对应公开详情打开函数、刷新大鱼 gallery、优先使用真实 gallery 命中项和写入错误提示;不要在壳层重新维护 `worldType` / `worldKey` 分支链。
|
||||||
|
|
||||||
|
|||||||
@@ -226,15 +226,7 @@ import {
|
|||||||
buildSquareHolePublicWorkCode,
|
buildSquareHolePublicWorkCode,
|
||||||
buildVisualNovelPublicWorkCode,
|
buildVisualNovelPublicWorkCode,
|
||||||
buildWoodenFishPublicWorkCode,
|
buildWoodenFishPublicWorkCode,
|
||||||
isSameBabyObjectMatchPublicWorkCode,
|
|
||||||
isSameBarkBattlePublicWorkCode,
|
|
||||||
isSameBigFishPublicWorkCode,
|
|
||||||
isSameJumpHopPublicWorkCode,
|
|
||||||
isSameMatch3DPublicWorkCode,
|
|
||||||
isSamePuzzlePublicWorkCode,
|
isSamePuzzlePublicWorkCode,
|
||||||
isSameSquareHolePublicWorkCode,
|
|
||||||
isSameVisualNovelPublicWorkCode,
|
|
||||||
isSameWoodenFishPublicWorkCode,
|
|
||||||
} from '../../services/publicWorkCode';
|
} from '../../services/publicWorkCode';
|
||||||
import {
|
import {
|
||||||
createPuzzleAgentSession,
|
createPuzzleAgentSession,
|
||||||
@@ -356,8 +348,6 @@ import {
|
|||||||
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
||||||
import {
|
import {
|
||||||
isEdutainmentGalleryEntry,
|
isEdutainmentGalleryEntry,
|
||||||
mapBabyObjectMatchDraftToPlatformGalleryCard,
|
|
||||||
mapBarkBattleWorkToPlatformGalleryCard,
|
|
||||||
mapPuzzleWorkToPlatformGalleryCard,
|
mapPuzzleWorkToPlatformGalleryCard,
|
||||||
type PlatformPublicGalleryCard,
|
type PlatformPublicGalleryCard,
|
||||||
resolvePlatformPublicWorkCode,
|
resolvePlatformPublicWorkCode,
|
||||||
@@ -550,8 +540,18 @@ import {
|
|||||||
resolveProfileWalletBalance,
|
resolveProfileWalletBalance,
|
||||||
} from './platformProfileWalletDeltaModel';
|
} from './platformProfileWalletDeltaModel';
|
||||||
import {
|
import {
|
||||||
|
mapRpgPublicCodeSearchDetailToGalleryCard,
|
||||||
type PlatformPublicCodeSearchStep,
|
type PlatformPublicCodeSearchStep,
|
||||||
|
resolveBabyObjectMatchPublicCodeSearchMatch,
|
||||||
|
resolveBarkBattlePublicCodeSearchMatch,
|
||||||
|
resolveBigFishPublicCodeSearchMatch,
|
||||||
|
resolveJumpHopPublicCodeSearchMatch,
|
||||||
|
resolveMatch3DPublicCodeSearchMatch,
|
||||||
resolvePlatformPublicCodeSearchPlan,
|
resolvePlatformPublicCodeSearchPlan,
|
||||||
|
resolvePuzzlePublicCodeSearchMatch,
|
||||||
|
resolveSquareHolePublicCodeSearchMatch,
|
||||||
|
resolveVisualNovelPublicCodeSearchMatch,
|
||||||
|
resolveWoodenFishPublicCodeSearchMatch,
|
||||||
} from './platformPublicCodeSearchModel';
|
} from './platformPublicCodeSearchModel';
|
||||||
import {
|
import {
|
||||||
buildPlatformPublicGalleryFeeds,
|
buildPlatformPublicGalleryFeeds,
|
||||||
@@ -12281,26 +12281,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const tryOpenGalleryEntry = async () => {
|
const tryOpenGalleryEntry = async () => {
|
||||||
const entry =
|
const entry =
|
||||||
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
||||||
const card = {
|
const card = mapRpgPublicCodeSearchDetailToGalleryCard(entry);
|
||||||
ownerUserId: entry.ownerUserId,
|
|
||||||
profileId: entry.profileId,
|
|
||||||
publicWorkCode: entry.publicWorkCode,
|
|
||||||
authorPublicUserCode: entry.authorPublicUserCode,
|
|
||||||
visibility: 'published',
|
|
||||||
publishedAt: entry.publishedAt,
|
|
||||||
updatedAt: entry.updatedAt,
|
|
||||||
authorDisplayName: entry.authorDisplayName,
|
|
||||||
worldName: entry.worldName,
|
|
||||||
subtitle: entry.subtitle,
|
|
||||||
summaryText: entry.summaryText,
|
|
||||||
coverImageSrc: entry.coverImageSrc,
|
|
||||||
themeMode: entry.themeMode,
|
|
||||||
playableNpcCount: entry.playableNpcCount,
|
|
||||||
landmarkCount: entry.landmarkCount,
|
|
||||||
playCount: entry.playCount ?? 0,
|
|
||||||
remixCount: entry.remixCount ?? 0,
|
|
||||||
likeCount: entry.likeCount ?? 0,
|
|
||||||
} satisfies CustomWorldGalleryCard;
|
|
||||||
if (!canExposePublicWork(card)) {
|
if (!canExposePublicWork(card)) {
|
||||||
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
|
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
|
||||||
}
|
}
|
||||||
@@ -12313,18 +12294,16 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
puzzleGalleryEntries.length > 0
|
puzzleGalleryEntries.length > 0
|
||||||
? puzzleGalleryEntries
|
? puzzleGalleryEntries
|
||||||
: await refreshPuzzleGallery();
|
: await refreshPuzzleGallery();
|
||||||
const matchedEntry = entries
|
const matchedEntry = resolvePuzzlePublicCodeSearchMatch(
|
||||||
.map(mapPuzzleWorkToPublicWorkDetail)
|
entries,
|
||||||
.filter(canExposePublicWork)
|
normalizedKeyword,
|
||||||
.find((entry) =>
|
|
||||||
isSamePuzzlePublicWorkCode(normalizedKeyword, entry.profileId),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到拼图作品。');
|
throw new Error('未找到拼图作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
await openPuzzlePublicWorkDetail(matchedEntry.profileId, {
|
await openPuzzlePublicWorkDetail(matchedEntry.detail.profileId, {
|
||||||
tab: platformBootstrap.platformTab,
|
tab: platformBootstrap.platformTab,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -12333,170 +12312,133 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
bigFishGalleryEntries.length > 0
|
bigFishGalleryEntries.length > 0
|
||||||
? bigFishGalleryEntries
|
? bigFishGalleryEntries
|
||||||
: await refreshBigFishGallery();
|
: await refreshBigFishGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveBigFishPublicCodeSearchMatch(
|
||||||
const detailEntry = mapBigFishWorkToPublicWorkDetail(entry);
|
entries,
|
||||||
return (
|
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameBigFishPublicWorkCode(
|
|
||||||
normalizedKeyword,
|
normalizedKeyword,
|
||||||
entry.sourceSessionId,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到大鱼吃小鱼作品。');
|
throw new Error('未找到大鱼吃小鱼作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(mapBigFishWorkToPublicWorkDetail(matchedEntry));
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
};
|
};
|
||||||
const tryOpenJumpHopGalleryEntry = async () => {
|
const tryOpenJumpHopGalleryEntry = async () => {
|
||||||
const entries =
|
const entries =
|
||||||
jumpHopGalleryEntries.length > 0
|
jumpHopGalleryEntries.length > 0
|
||||||
? jumpHopGalleryEntries
|
? jumpHopGalleryEntries
|
||||||
: await refreshJumpHopGallery();
|
: await refreshJumpHopGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveJumpHopPublicCodeSearchMatch(
|
||||||
const detailEntry = mapJumpHopWorkToPublicWorkDetail(entry);
|
entries,
|
||||||
return (
|
normalizedKeyword,
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameJumpHopPublicWorkCode(normalizedKeyword, entry.profileId)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到跳一跳作品。');
|
throw new Error('未找到跳一跳作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(mapJumpHopWorkToPublicWorkDetail(matchedEntry));
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
};
|
};
|
||||||
const tryOpenWoodenFishGalleryEntry = async () => {
|
const tryOpenWoodenFishGalleryEntry = async () => {
|
||||||
const entries =
|
const entries =
|
||||||
woodenFishGalleryEntries.length > 0
|
woodenFishGalleryEntries.length > 0
|
||||||
? woodenFishGalleryEntries
|
? woodenFishGalleryEntries
|
||||||
: await refreshWoodenFishGallery();
|
: await refreshWoodenFishGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveWoodenFishPublicCodeSearchMatch(
|
||||||
const detailEntry = mapWoodenFishWorkToPublicWorkDetail(entry);
|
entries,
|
||||||
return (
|
normalizedKeyword,
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameWoodenFishPublicWorkCode(normalizedKeyword, entry.profileId)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到敲木鱼作品。');
|
throw new Error('未找到敲木鱼作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(mapWoodenFishWorkToPublicWorkDetail(matchedEntry));
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
};
|
};
|
||||||
const tryOpenMatch3DGalleryEntry = async () => {
|
const tryOpenMatch3DGalleryEntry = async () => {
|
||||||
const entries =
|
const entries =
|
||||||
match3dGalleryEntries.length > 0
|
match3dGalleryEntries.length > 0
|
||||||
? match3dGalleryEntries
|
? match3dGalleryEntries
|
||||||
: await refreshMatch3DGallery();
|
: await refreshMatch3DGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveMatch3DPublicCodeSearchMatch(
|
||||||
const detailEntry = mapMatch3DWorkToPublicWorkDetail(entry);
|
entries,
|
||||||
return (
|
normalizedKeyword,
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameMatch3DPublicWorkCode(normalizedKeyword, entry.profileId)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到抓大鹅作品。');
|
throw new Error('未找到抓大鹅作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(matchedEntry));
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
};
|
};
|
||||||
const tryOpenSquareHoleGalleryEntry = async () => {
|
const tryOpenSquareHoleGalleryEntry = async () => {
|
||||||
const entries =
|
const entries =
|
||||||
squareHoleGalleryEntries.length > 0
|
squareHoleGalleryEntries.length > 0
|
||||||
? squareHoleGalleryEntries
|
? squareHoleGalleryEntries
|
||||||
: await refreshSquareHoleGallery();
|
: await refreshSquareHoleGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveSquareHolePublicCodeSearchMatch(
|
||||||
const detailEntry = mapSquareHoleWorkToPublicWorkDetail(entry);
|
entries,
|
||||||
return (
|
normalizedKeyword,
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameSquareHolePublicWorkCode(normalizedKeyword, entry.profileId)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到方洞挑战作品。');
|
throw new Error('未找到方洞挑战作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(matchedEntry));
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
};
|
};
|
||||||
const tryOpenVisualNovelGalleryEntry = async () => {
|
const tryOpenVisualNovelGalleryEntry = async () => {
|
||||||
const entries =
|
const entries =
|
||||||
visualNovelGalleryEntries.length > 0
|
visualNovelGalleryEntries.length > 0
|
||||||
? visualNovelGalleryEntries
|
? visualNovelGalleryEntries
|
||||||
: await refreshVisualNovelGallery();
|
: await refreshVisualNovelGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveVisualNovelPublicCodeSearchMatch(
|
||||||
const detailEntry = mapVisualNovelWorkToPublicWorkDetail(entry);
|
entries,
|
||||||
return (
|
normalizedKeyword,
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameVisualNovelPublicWorkCode(normalizedKeyword, entry.profileId)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到视觉小说作品。');
|
throw new Error('未找到视觉小说作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
mapVisualNovelWorkToPublicWorkDetail(matchedEntry),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
const tryOpenBabyObjectMatchGalleryEntry = async () => {
|
const tryOpenBabyObjectMatchGalleryEntry = async () => {
|
||||||
const entries = (await listLocalBabyObjectMatchDrafts()).filter(
|
const entries = (await listLocalBabyObjectMatchDrafts()).filter(
|
||||||
(draft) => draft.publicationStatus === 'published',
|
(draft) => draft.publicationStatus === 'published',
|
||||||
);
|
);
|
||||||
const matchedDraft = entries.find((draft) => {
|
const matchedDraft = resolveBabyObjectMatchPublicCodeSearchMatch(
|
||||||
const detailEntry =
|
entries,
|
||||||
mapBabyObjectMatchDraftToPlatformGalleryCard(draft);
|
|
||||||
return (
|
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameBabyObjectMatchPublicWorkCode(
|
|
||||||
normalizedKeyword,
|
normalizedKeyword,
|
||||||
draft.profileId,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedDraft) {
|
if (!matchedDraft) {
|
||||||
throw new Error('未找到宝贝识物作品。');
|
throw new Error('未找到宝贝识物作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailEntry =
|
setBabyObjectMatchDraft(matchedDraft.item);
|
||||||
mapBabyObjectMatchDraftToPlatformGalleryCard(matchedDraft);
|
openPublicWorkDetail(matchedDraft.detail);
|
||||||
setBabyObjectMatchDraft(matchedDraft);
|
|
||||||
openPublicWorkDetail(detailEntry);
|
|
||||||
};
|
};
|
||||||
const tryOpenBarkBattleGalleryEntry = async () => {
|
const tryOpenBarkBattleGalleryEntry = async () => {
|
||||||
const entries =
|
const entries =
|
||||||
barkBattleGalleryEntries.length > 0
|
barkBattleGalleryEntries.length > 0
|
||||||
? barkBattleGalleryEntries
|
? barkBattleGalleryEntries
|
||||||
: await refreshBarkBattleGallery();
|
: await refreshBarkBattleGallery();
|
||||||
const matchedEntry = entries.find((entry) => {
|
const matchedEntry = resolveBarkBattlePublicCodeSearchMatch(
|
||||||
const detailEntry = mapBarkBattleWorkToPlatformGalleryCard(entry);
|
entries,
|
||||||
return (
|
normalizedKeyword,
|
||||||
canExposePublicWork(detailEntry) &&
|
|
||||||
isSameBarkBattlePublicWorkCode(normalizedKeyword, entry.workId)
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchedEntry) {
|
if (!matchedEntry) {
|
||||||
throw new Error('未找到汪汪声浪作品。');
|
throw new Error('未找到汪汪声浪作品。');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionStage === 'bark-battle-runtime') {
|
if (selectionStage === 'bark-battle-runtime') {
|
||||||
startBarkBattleRunFromWork(matchedEntry, 'platform');
|
startBarkBattleRunFromWork(matchedEntry.item, 'platform');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openPublicWorkDetail(
|
openPublicWorkDetail(matchedEntry.detail);
|
||||||
mapBarkBattleWorkToPlatformGalleryCard(matchedEntry),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const runSearchStep = async (step: PlatformPublicCodeSearchStep) => {
|
const runSearchStep = async (step: PlatformPublicCodeSearchStep) => {
|
||||||
|
|||||||
@@ -1,8 +1,39 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
|
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||||
|
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||||
|
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||||
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
|
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
|
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||||
|
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
import {
|
import {
|
||||||
|
buildBarkBattlePublicWorkCode,
|
||||||
|
buildBigFishPublicWorkCode,
|
||||||
|
buildJumpHopPublicWorkCode,
|
||||||
|
buildMatch3DPublicWorkCode,
|
||||||
|
buildPuzzlePublicWorkCode,
|
||||||
|
buildSquareHolePublicWorkCode,
|
||||||
|
buildVisualNovelPublicWorkCode,
|
||||||
|
buildWoodenFishPublicWorkCode,
|
||||||
|
} from '../../services/publicWorkCode';
|
||||||
|
import type { CustomWorldProfile } from '../../types';
|
||||||
|
import {
|
||||||
|
mapRpgPublicCodeSearchDetailToGalleryCard,
|
||||||
type PlatformPublicCodeSearchStep,
|
type PlatformPublicCodeSearchStep,
|
||||||
|
resolveBabyObjectMatchPublicCodeSearchMatch,
|
||||||
|
resolveBarkBattlePublicCodeSearchMatch,
|
||||||
|
resolveBigFishPublicCodeSearchMatch,
|
||||||
|
resolveJumpHopPublicCodeSearchMatch,
|
||||||
|
resolveMatch3DPublicCodeSearchMatch,
|
||||||
resolvePlatformPublicCodeSearchPlan,
|
resolvePlatformPublicCodeSearchPlan,
|
||||||
|
resolvePuzzlePublicCodeSearchMatch,
|
||||||
|
resolveSquareHolePublicCodeSearchMatch,
|
||||||
|
resolveVisualNovelPublicCodeSearchMatch,
|
||||||
|
resolveWoodenFishPublicCodeSearchMatch,
|
||||||
} from './platformPublicCodeSearchModel';
|
} from './platformPublicCodeSearchModel';
|
||||||
|
|
||||||
function expectSearchSteps(
|
function expectSearchSteps(
|
||||||
@@ -66,4 +97,386 @@ describe('platformPublicCodeSearchModel', () => {
|
|||||||
expectSearchSteps('SY-00000001', legacyFallbackSteps);
|
expectSearchSteps('SY-00000001', legacyFallbackSteps);
|
||||||
expectSearchSteps('月井守望', legacyFallbackSteps);
|
expectSearchSteps('月井守望', legacyFallbackSteps);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('maps RPG detail responses to gallery cards with count defaults', () => {
|
||||||
|
expect(
|
||||||
|
mapRpgPublicCodeSearchDetailToGalleryCard(
|
||||||
|
buildRpgDetailEntry({
|
||||||
|
playCount: undefined,
|
||||||
|
remixCount: undefined,
|
||||||
|
likeCount: undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
profileId: 'rpg-profile-1',
|
||||||
|
visibility: 'published',
|
||||||
|
worldName: '潮雾世界',
|
||||||
|
playCount: 0,
|
||||||
|
remixCount: 0,
|
||||||
|
likeCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves public code matches for every play-specific gallery type', () => {
|
||||||
|
const puzzle = buildPuzzleWork({ profileId: 'puzzle-profile-12345678' });
|
||||||
|
const bigFish = buildBigFishWork({
|
||||||
|
sourceSessionId: 'big-fish-session-12345678',
|
||||||
|
});
|
||||||
|
const jumpHop = buildJumpHopCard({ profileId: 'jump-hop-profile-12345678' });
|
||||||
|
const woodenFish = buildWoodenFishCard({
|
||||||
|
profileId: 'wooden-fish-profile-12345678',
|
||||||
|
});
|
||||||
|
const babyObjectMatch = buildBabyObjectMatchDraft({
|
||||||
|
profileId: 'baby-object-profile-12345678',
|
||||||
|
});
|
||||||
|
const match3d = buildMatch3DWork({ profileId: 'match3d-profile-12345678' });
|
||||||
|
const squareHole = buildSquareHoleWork({
|
||||||
|
profileId: 'square-hole-profile-12345678',
|
||||||
|
});
|
||||||
|
const visualNovel = buildVisualNovelWork({
|
||||||
|
profileId: 'visual-novel-profile-12345678',
|
||||||
|
});
|
||||||
|
const barkBattle = buildBarkBattleWork({
|
||||||
|
workId: 'bark-battle-work-12345678',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePublicCodeSearchMatch(
|
||||||
|
[puzzle],
|
||||||
|
buildPuzzlePublicWorkCode(puzzle.profileId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'puzzle' });
|
||||||
|
expect(
|
||||||
|
resolveBigFishPublicCodeSearchMatch(
|
||||||
|
[bigFish],
|
||||||
|
buildBigFishPublicWorkCode(bigFish.sourceSessionId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'big-fish' });
|
||||||
|
expect(
|
||||||
|
resolveJumpHopPublicCodeSearchMatch(
|
||||||
|
[jumpHop],
|
||||||
|
buildJumpHopPublicWorkCode(jumpHop.profileId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'jump-hop' });
|
||||||
|
expect(
|
||||||
|
resolveWoodenFishPublicCodeSearchMatch(
|
||||||
|
[woodenFish],
|
||||||
|
buildWoodenFishPublicWorkCode(woodenFish.profileId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'wooden-fish' });
|
||||||
|
expect(
|
||||||
|
resolveBabyObjectMatchPublicCodeSearchMatch(
|
||||||
|
[babyObjectMatch],
|
||||||
|
`BO-${babyObjectMatch.profileId.slice(-8)}`,
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'edutainment' });
|
||||||
|
expect(
|
||||||
|
resolveMatch3DPublicCodeSearchMatch(
|
||||||
|
[match3d],
|
||||||
|
buildMatch3DPublicWorkCode(match3d.profileId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'match3d' });
|
||||||
|
expect(
|
||||||
|
resolveSquareHolePublicCodeSearchMatch(
|
||||||
|
[squareHole],
|
||||||
|
buildSquareHolePublicWorkCode(squareHole.profileId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'square-hole' });
|
||||||
|
expect(
|
||||||
|
resolveVisualNovelPublicCodeSearchMatch(
|
||||||
|
[visualNovel],
|
||||||
|
buildVisualNovelPublicWorkCode(visualNovel.profileId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'visual-novel' });
|
||||||
|
expect(
|
||||||
|
resolveBarkBattlePublicCodeSearchMatch(
|
||||||
|
[barkBattle],
|
||||||
|
buildBarkBattlePublicWorkCode(barkBattle.workId),
|
||||||
|
)?.detail,
|
||||||
|
).toMatchObject({ sourceType: 'bark-battle' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('public code search matchers skip entries hidden by visibility policy', () => {
|
||||||
|
const hiddenPuzzle = buildPuzzleWork({
|
||||||
|
profileId: 'hidden-profile-12345678',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePublicCodeSearchMatch(
|
||||||
|
[hiddenPuzzle],
|
||||||
|
buildPuzzlePublicWorkCode(hiddenPuzzle.profileId),
|
||||||
|
() => false,
|
||||||
|
),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildRpgDetailEntry(
|
||||||
|
overrides: Partial<CustomWorldLibraryEntry<CustomWorldProfile>> = {},
|
||||||
|
): CustomWorldLibraryEntry<CustomWorldProfile> {
|
||||||
|
return {
|
||||||
|
ownerUserId: 'rpg-owner-1',
|
||||||
|
profileId: 'rpg-profile-1',
|
||||||
|
publicWorkCode: 'CW-00000001',
|
||||||
|
authorPublicUserCode: 'SY-00000001',
|
||||||
|
profile: {} as CustomWorldProfile,
|
||||||
|
visibility: 'published',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
worldName: '潮雾世界',
|
||||||
|
subtitle: '潮雾港',
|
||||||
|
summaryText: '潮雾世界说明。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeMode: 'tide',
|
||||||
|
playableNpcCount: 1,
|
||||||
|
landmarkCount: 1,
|
||||||
|
playCount: 1,
|
||||||
|
remixCount: 1,
|
||||||
|
likeCount: 1,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPuzzleWork(
|
||||||
|
overrides: Partial<PuzzleWorkSummary> = {},
|
||||||
|
): PuzzleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'puzzle-work-1',
|
||||||
|
profileId: 'puzzle-profile-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
sourceSessionId: 'puzzle-session-1',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
workTitle: '潮雾拼图',
|
||||||
|
workDescription: '潮雾拼图说明。',
|
||||||
|
levelName: '潮雾拼图',
|
||||||
|
summary: '潮雾拼图说明。',
|
||||||
|
themeTags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
coverAssetId: null,
|
||||||
|
publicationStatus: 'published',
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
playCount: 0,
|
||||||
|
remixCount: 0,
|
||||||
|
likeCount: 0,
|
||||||
|
publishReady: true,
|
||||||
|
levels: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBigFishWork(
|
||||||
|
overrides: Partial<BigFishWorkSummary> = {},
|
||||||
|
): BigFishWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'big-fish-work-1',
|
||||||
|
sourceSessionId: 'big-fish-session-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
title: '潮雾大鱼',
|
||||||
|
subtitle: '潮雾港',
|
||||||
|
summary: '潮雾大鱼说明。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
playCount: 0,
|
||||||
|
remixCount: 0,
|
||||||
|
likeCount: 0,
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 1,
|
||||||
|
levelMainImageReadyCount: 1,
|
||||||
|
levelMotionReadyCount: 1,
|
||||||
|
backgroundReady: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJumpHopCard(
|
||||||
|
overrides: Partial<JumpHopGalleryCardResponse> = {},
|
||||||
|
): JumpHopGalleryCardResponse {
|
||||||
|
const profileId = overrides.profileId ?? 'jump-hop-profile-1';
|
||||||
|
return {
|
||||||
|
publicWorkCode: buildJumpHopPublicWorkCode(profileId),
|
||||||
|
workId: 'jump-hop-work-1',
|
||||||
|
profileId,
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
workTitle: '潮雾跳一跳',
|
||||||
|
workDescription: '潮雾跳一跳说明。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeTags: [],
|
||||||
|
difficulty: 'standard',
|
||||||
|
stylePreset: 'minimal-blocks',
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWoodenFishCard(
|
||||||
|
overrides: Partial<WoodenFishGalleryCardResponse> = {},
|
||||||
|
): WoodenFishGalleryCardResponse {
|
||||||
|
const profileId = overrides.profileId ?? 'wooden-fish-profile-1';
|
||||||
|
return {
|
||||||
|
publicWorkCode: buildWoodenFishPublicWorkCode(profileId),
|
||||||
|
workId: 'wooden-fish-work-1',
|
||||||
|
profileId,
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
workTitle: '潮雾木鱼',
|
||||||
|
workDescription: '潮雾木鱼说明。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeTags: ['敲木鱼'],
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBabyObjectMatchDraft(
|
||||||
|
overrides: Partial<BabyObjectMatchDraft> = {},
|
||||||
|
): BabyObjectMatchDraft {
|
||||||
|
return {
|
||||||
|
draftId: 'baby-draft-1',
|
||||||
|
profileId: 'baby-object-profile-1',
|
||||||
|
templateId: 'baby-object-match',
|
||||||
|
templateName: '宝贝识物',
|
||||||
|
workTitle: '潮雾识物',
|
||||||
|
workDescription: '潮雾识物说明。',
|
||||||
|
itemNames: ['苹果', '香蕉'],
|
||||||
|
itemAssets: [
|
||||||
|
buildBabyObjectMatchItemAsset('item-a', '苹果'),
|
||||||
|
buildBabyObjectMatchItemAsset('item-b', '香蕉'),
|
||||||
|
],
|
||||||
|
visualPackage: null,
|
||||||
|
themeTags: ['寓教于乐'],
|
||||||
|
publicationStatus: 'published',
|
||||||
|
createdAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBabyObjectMatchItemAsset(itemId: string, itemName: string) {
|
||||||
|
return {
|
||||||
|
itemId,
|
||||||
|
itemName,
|
||||||
|
imageSrc: `/media/${itemId}.png`,
|
||||||
|
assetObjectId: null,
|
||||||
|
generationProvider: 'placeholder' as const,
|
||||||
|
prompt: itemName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMatch3DWork(
|
||||||
|
overrides: Partial<Match3DWorkSummary> = {},
|
||||||
|
): Match3DWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'match3d-work-1',
|
||||||
|
profileId: 'match3d-profile-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
sourceSessionId: 'match3d-session-1',
|
||||||
|
gameName: '潮雾抓大鹅',
|
||||||
|
themeText: '潮雾港',
|
||||||
|
summary: '潮雾抓大鹅说明。',
|
||||||
|
tags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
referenceImageSrc: null,
|
||||||
|
clearCount: 0,
|
||||||
|
difficulty: 1,
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
generationStatus: 'ready',
|
||||||
|
generatedItemAssets: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSquareHoleWork(
|
||||||
|
overrides: Partial<SquareHoleWorkSummary> = {},
|
||||||
|
): SquareHoleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'square-hole-work-1',
|
||||||
|
profileId: 'square-hole-profile-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
sourceSessionId: 'square-hole-session-1',
|
||||||
|
gameName: '潮雾方洞',
|
||||||
|
themeText: '潮雾港',
|
||||||
|
twistRule: '避开雾门',
|
||||||
|
summary: '潮雾方洞说明。',
|
||||||
|
tags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
backgroundPrompt: '潮雾港',
|
||||||
|
backgroundImageSrc: null,
|
||||||
|
shapeOptions: [],
|
||||||
|
holeOptions: [],
|
||||||
|
shapeCount: 1,
|
||||||
|
difficulty: 1,
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVisualNovelWork(
|
||||||
|
overrides: Partial<VisualNovelWorkSummary> = {},
|
||||||
|
): VisualNovelWorkSummary {
|
||||||
|
return {
|
||||||
|
runtimeKind: 'visual-novel',
|
||||||
|
profileId: 'visual-novel-profile-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
title: '潮雾视觉小说',
|
||||||
|
description: '潮雾视觉小说说明。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
tags: [],
|
||||||
|
publishStatus: 'published',
|
||||||
|
publishReady: true,
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBarkBattleWork(
|
||||||
|
overrides: Partial<BarkBattleWorkSummary> = {},
|
||||||
|
): BarkBattleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'bark-battle-work-1',
|
||||||
|
draftId: 'bark-battle-draft-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
title: '潮雾声浪',
|
||||||
|
summary: '潮雾声浪说明。',
|
||||||
|
themeDescription: '潮雾港',
|
||||||
|
playerImageDescription: '小狗',
|
||||||
|
opponentImageDescription: '对手',
|
||||||
|
onomatopoeia: ['汪'],
|
||||||
|
playerCharacterImageSrc: null,
|
||||||
|
opponentCharacterImageSrc: null,
|
||||||
|
uiBackgroundImageSrc: null,
|
||||||
|
difficultyPreset: 'normal',
|
||||||
|
status: 'published',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
publishReady: true,
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
|
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||||
|
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||||
|
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||||
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
|
import type {
|
||||||
|
CustomWorldGalleryCard,
|
||||||
|
CustomWorldLibraryEntry,
|
||||||
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
|
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||||
|
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
|
import {
|
||||||
|
isSameBabyObjectMatchPublicWorkCode,
|
||||||
|
isSameBarkBattlePublicWorkCode,
|
||||||
|
isSameBigFishPublicWorkCode,
|
||||||
|
isSameJumpHopPublicWorkCode,
|
||||||
|
isSameMatch3DPublicWorkCode,
|
||||||
|
isSamePuzzlePublicWorkCode,
|
||||||
|
isSameSquareHolePublicWorkCode,
|
||||||
|
isSameVisualNovelPublicWorkCode,
|
||||||
|
isSameWoodenFishPublicWorkCode,
|
||||||
|
} from '../../services/publicWorkCode';
|
||||||
|
import type { CustomWorldProfile } from '../../types';
|
||||||
|
import type { PlatformPublicGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
|
import { mapBabyObjectMatchDraftToPlatformGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
|
import { canExposePublicWork } from './platformEdutainmentVisibility';
|
||||||
|
import { mapMatch3DWorkToPublicWorkDetail } from './platformMatch3DRuntimeProfile';
|
||||||
|
import {
|
||||||
|
mapBarkBattleWorkToPublicWorkDetail,
|
||||||
|
mapBigFishWorkToPublicWorkDetail,
|
||||||
|
mapJumpHopWorkToPublicWorkDetail,
|
||||||
|
mapPuzzleWorkToPublicWorkDetail,
|
||||||
|
mapSquareHoleWorkToPublicWorkDetail,
|
||||||
|
mapVisualNovelWorkToPublicWorkDetail,
|
||||||
|
mapWoodenFishWorkToPublicWorkDetail,
|
||||||
|
} from './platformPublicWorkDetailFlow';
|
||||||
|
|
||||||
export type PlatformPublicCodeSearchStep =
|
export type PlatformPublicCodeSearchStep =
|
||||||
| 'user-id'
|
| 'user-id'
|
||||||
| 'public-user-code'
|
| 'public-user-code'
|
||||||
@@ -17,6 +56,19 @@ export type PlatformPublicCodeSearchPlan = {
|
|||||||
steps: readonly PlatformPublicCodeSearchStep[];
|
steps: readonly PlatformPublicCodeSearchStep[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PlatformPublicCodeSearchMatch<TItem> = {
|
||||||
|
item: TItem;
|
||||||
|
detail: PlatformPublicGalleryCard;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlatformPublicCodeSearchMatcherInput<TItem> = {
|
||||||
|
keyword: string;
|
||||||
|
entries: readonly TItem[];
|
||||||
|
mapEntry: (item: TItem) => PlatformPublicGalleryCard;
|
||||||
|
matchesEntry: (keyword: string, item: TItem) => boolean;
|
||||||
|
canExposeEntry?: (entry: PlatformPublicGalleryCard) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const PLATFORM_PUBLIC_USER_ID_PATTERN = /^user[_-][a-z0-9_-]+$/iu;
|
const PLATFORM_PUBLIC_USER_ID_PATTERN = /^user[_-][a-z0-9_-]+$/iu;
|
||||||
const PLATFORM_RPG_WORK_NUMERIC_CODE_PATTERN = /^\d{1,8}$/u;
|
const PLATFORM_RPG_WORK_NUMERIC_CODE_PATTERN = /^\d{1,8}$/u;
|
||||||
|
|
||||||
@@ -81,3 +133,181 @@ export function resolvePlatformPublicCodeSearchPlan(
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapRpgPublicCodeSearchDetailToGalleryCard(
|
||||||
|
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
||||||
|
): CustomWorldGalleryCard {
|
||||||
|
return {
|
||||||
|
ownerUserId: entry.ownerUserId,
|
||||||
|
profileId: entry.profileId,
|
||||||
|
publicWorkCode: entry.publicWorkCode,
|
||||||
|
authorPublicUserCode: entry.authorPublicUserCode,
|
||||||
|
visibility: 'published',
|
||||||
|
publishedAt: entry.publishedAt,
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
authorDisplayName: entry.authorDisplayName,
|
||||||
|
worldName: entry.worldName,
|
||||||
|
subtitle: entry.subtitle,
|
||||||
|
summaryText: entry.summaryText,
|
||||||
|
coverImageSrc: entry.coverImageSrc,
|
||||||
|
themeMode: entry.themeMode,
|
||||||
|
playableNpcCount: entry.playableNpcCount,
|
||||||
|
landmarkCount: entry.landmarkCount,
|
||||||
|
playCount: entry.playCount ?? 0,
|
||||||
|
remixCount: entry.remixCount ?? 0,
|
||||||
|
likeCount: entry.likeCount ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePuzzlePublicCodeSearchMatch(
|
||||||
|
entries: readonly PuzzleWorkSummary[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapPuzzleWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSamePuzzlePublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBigFishPublicCodeSearchMatch(
|
||||||
|
entries: readonly BigFishWorkSummary[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapBigFishWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameBigFishPublicWorkCode(searchKeyword, item.sourceSessionId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveJumpHopPublicCodeSearchMatch(
|
||||||
|
entries: readonly JumpHopGalleryCardResponse[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapJumpHopWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameJumpHopPublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWoodenFishPublicCodeSearchMatch(
|
||||||
|
entries: readonly WoodenFishGalleryCardResponse[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapWoodenFishWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameWoodenFishPublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBabyObjectMatchPublicCodeSearchMatch(
|
||||||
|
entries: readonly BabyObjectMatchDraft[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapBabyObjectMatchDraftToPlatformGalleryCard,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameBabyObjectMatchPublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMatch3DPublicCodeSearchMatch(
|
||||||
|
entries: readonly Match3DWorkSummary[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapMatch3DWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameMatch3DPublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSquareHolePublicCodeSearchMatch(
|
||||||
|
entries: readonly SquareHoleWorkSummary[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapSquareHoleWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameSquareHolePublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveVisualNovelPublicCodeSearchMatch(
|
||||||
|
entries: readonly VisualNovelWorkSummary[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapVisualNovelWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameVisualNovelPublicWorkCode(searchKeyword, item.profileId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBarkBattlePublicCodeSearchMatch(
|
||||||
|
entries: readonly BarkBattleWorkSummary[],
|
||||||
|
keyword: string,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
) {
|
||||||
|
return resolveMappedPublicCodeSearchMatch({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry: mapBarkBattleWorkToPublicWorkDetail,
|
||||||
|
matchesEntry: (searchKeyword, item) =>
|
||||||
|
isSameBarkBattlePublicWorkCode(searchKeyword, item.workId),
|
||||||
|
canExposeEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMappedPublicCodeSearchMatch<TItem>({
|
||||||
|
keyword,
|
||||||
|
entries,
|
||||||
|
mapEntry,
|
||||||
|
matchesEntry,
|
||||||
|
canExposeEntry = canExposePublicWork,
|
||||||
|
}: PlatformPublicCodeSearchMatcherInput<TItem>):
|
||||||
|
| PlatformPublicCodeSearchMatch<TItem>
|
||||||
|
| null {
|
||||||
|
for (const item of entries) {
|
||||||
|
const detail = mapEntry(item);
|
||||||
|
if (canExposeEntry(detail) && matchesEntry(keyword, item)) {
|
||||||
|
return { item, detail };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
buildCustomWorldPublicWorkCode,
|
buildCustomWorldPublicWorkCode,
|
||||||
buildJumpHopPublicWorkCode,
|
buildJumpHopPublicWorkCode,
|
||||||
|
buildMatch3DPublicWorkCode,
|
||||||
buildWoodenFishPublicWorkCode,
|
buildWoodenFishPublicWorkCode,
|
||||||
isSameCustomWorldPublicWorkCode,
|
isSameCustomWorldPublicWorkCode,
|
||||||
isSameJumpHopPublicWorkCode,
|
isSameJumpHopPublicWorkCode,
|
||||||
|
isSameMatch3DPublicWorkCode,
|
||||||
isSameWoodenFishPublicWorkCode,
|
isSameWoodenFishPublicWorkCode,
|
||||||
} from './publicWorkCode';
|
} from './publicWorkCode';
|
||||||
|
|
||||||
@@ -34,6 +36,24 @@ describe('publicWorkCode', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches current and legacy match3d public work prefixes', () => {
|
||||||
|
expect(buildMatch3DPublicWorkCode('match3d-profile-12345678')).toBe(
|
||||||
|
'M3-12345678',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isSameMatch3DPublicWorkCode(
|
||||||
|
'M3-12345678',
|
||||||
|
'match3d-profile-12345678',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isSameMatch3DPublicWorkCode(
|
||||||
|
'M3D-12345678',
|
||||||
|
'match3d-profile-12345678',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('builds and matches custom world public work codes from profile ids', () => {
|
it('builds and matches custom world public work codes from profile ids', () => {
|
||||||
expect(buildCustomWorldPublicWorkCode('world-public-1')).toBe('CW-00000001');
|
expect(buildCustomWorldPublicWorkCode('world-public-1')).toBe('CW-00000001');
|
||||||
expect(isSameCustomWorldPublicWorkCode('cw-00000001', 'world-public-1')).toBe(
|
expect(isSameCustomWorldPublicWorkCode('cw-00000001', 'world-public-1')).toBe(
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ export function buildMatch3DPublicWorkCode(profileId: string) {
|
|||||||
return `M3-${suffix}`;
|
return `M3-${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLegacyMatch3DPublicWorkCode(profileId: string) {
|
||||||
|
const normalized = normalizePublicCodeText(profileId);
|
||||||
|
const fallback = normalized || '00000000';
|
||||||
|
const suffix = fallback.slice(-8).padStart(8, '0');
|
||||||
|
|
||||||
|
return `M3D-${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSquareHolePublicWorkCode(profileId: string) {
|
export function buildSquareHolePublicWorkCode(profileId: string) {
|
||||||
const normalized = normalizePublicCodeText(profileId);
|
const normalized = normalizePublicCodeText(profileId);
|
||||||
const fallback = normalized || '00000000';
|
const fallback = normalized || '00000000';
|
||||||
@@ -134,6 +142,8 @@ export function isSameMatch3DPublicWorkCode(keyword: string, profileId: string)
|
|||||||
return (
|
return (
|
||||||
normalizedKeyword ===
|
normalizedKeyword ===
|
||||||
normalizePublicCodeText(buildMatch3DPublicWorkCode(profileId)) ||
|
normalizePublicCodeText(buildMatch3DPublicWorkCode(profileId)) ||
|
||||||
|
normalizedKeyword ===
|
||||||
|
normalizePublicCodeText(buildLegacyMatch3DPublicWorkCode(profileId)) ||
|
||||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user