修复导览点位可见性与构建门禁

This commit is contained in:
cxk
2026-07-20 23:54:19 +08:00
parent 735c5cd1ee
commit 1f89e8b3e0
14 changed files with 499 additions and 168 deletions

View File

@@ -5,7 +5,11 @@ import {
} from '@/domain/poiCategories'
import type { SgsSdkApiProvider, SgsSdkManifestPayload } from '@/data/providers/sgsSdkApiProvider'
import type { StaticNavAssetsProvider } from '@/data/providers/staticNavAssetsProvider'
import { SgsSdkGuideRepository, StaticGuideRepository } from '@/repositories/GuideRepository'
import {
canonicalizeVisitorSearchPois,
SgsSdkGuideRepository,
StaticGuideRepository
} from '@/repositories/GuideRepository'
import {
SgsSdkGuideModelRepository,
StaticGuideModelRepository
@@ -40,7 +44,17 @@ const floorSpaces = [
{ id: 'space-hall', name: '地球展厅', type: 'exhibition_hall', floorId: 'L1', center: { x: 1, y: 12, z: 1 } },
{ id: 'space-cinema', name: '穹幕影院', type: 'theater', floorId: 'L1', center: { x: 2, y: 12, z: 2 } },
{ id: 'space-dining', name: '餐饮区', type: 'restaurant', floorId: 'L1', center: { x: 3, y: 12, z: 3 } },
{ id: 'space-shopping', name: '文创商店', type: 'commercial', floorId: 'L1', center: { x: 4, y: 12, z: 4 } }
{ id: 'space-shopping', name: '文创商店', type: 'commercial', floorId: 'L1', center: { x: 4, y: 12, z: 4 } },
{ id: 'space-private-tea', name: '茶水间', type: 'service_space', floorId: 'L1', center: { x: 5, y: 12, z: 5 } },
{ id: 'space-private-vip', name: '贵宾接待区', type: 'service_space', floorId: 'L1', center: { x: 6, y: 12, z: 6 } },
{
id: 'space-public-rest',
name: '游客休息区',
type: 'service_space',
floorId: 'L1',
center: { x: 7, y: 12, z: 7 },
visitorVisible: true
}
]
const floorPois = [
@@ -120,17 +134,25 @@ describe('GuideRepository visitor search contracts', () => {
const provider = createSgsProvider()
const repository = new SgsSdkGuideRepository(provider)
const [defaultResults, availability] = await Promise.all([
const [defaultResults, availability, privateKeyword, publicKeyword] = await Promise.all([
repository.listDestinationPois('L1'),
repository.getQuickFindCategoryAvailability('L1')
repository.getQuickFindCategoryAvailability('L1'),
repository.searchPois('茶水间', 'L1'),
repository.searchPois('游客休息区', 'L1')
])
expect(defaultResults.map((poi) => poi.id)).toEqual(expect.arrayContaining([
'hall-space-hall',
'hall-space-cinema',
'space-space-dining',
'space-space-shopping'
'space-space-shopping',
'space-space-public-rest'
]))
for (const privatePoiId of ['space-space-private-tea', 'space-space-private-vip']) {
expect(defaultResults.map((poi) => poi.id)).not.toContain(privatePoiId)
}
expect(privateKeyword).toEqual([])
expect(publicKeyword.map((poi) => poi.id)).toEqual(['space-space-public-rest'])
expect(defaultResults.map((poi) => poi.id)).not.toEqual(expect.arrayContaining([
'restroom',
'elevator',
@@ -160,9 +182,13 @@ describe('GuideRepository visitor search contracts', () => {
// survive model marker deduplication.
expect(renderPoiIds.has('restroom')).toBe(true)
expect(renderPoiIds.has('elevator')).toBe(true)
expect(renderPoiIds.has('space-space-public-rest')).toBe(true)
for (const hiddenPoiId of ['door', 'anchor', 'route-node', 'guide-stop']) {
expect(renderPoiIds.has(hiddenPoiId)).toBe(false)
}
for (const privatePoiId of ['space-space-private-tea', 'space-space-private-vip']) {
expect(renderPoiIds.has(privatePoiId)).toBe(false)
}
const quickResultIds = (await Promise.all(allQuickCategoryIds.map((categoryId) => (
repository.listQuickFindPois(categoryId, 'L1')
)))).flat().map((poi) => poi.id)
@@ -188,6 +214,115 @@ describe('GuideRepository visitor search contracts', () => {
expect(hiddenKeyword).toEqual([])
})
it('does not resolve private direct IDs outside the visitor search pool', async () => {
const repository = new SgsSdkGuideRepository(createSgsProvider({
queryPois: vi.fn().mockResolvedValue([{
id: 'private-direct-space',
name: '茶水间',
type: 'service_space',
floorId: 'L1',
visitorVisible: false,
position: { x: 30, y: 12, z: 30 }
}])
}))
await expect(repository.getPoiById('private-direct-space')).resolves.toBeNull()
})
it('keeps private and structural space duplicates out of search lists and map markers', async () => {
const provider = createSgsProvider({
getFloorPois: vi.fn().mockResolvedValue([
{
id: 'poi-private-dining',
name: '西侧餐饮区',
type: 'restaurant',
floorId: 'L1',
spatialAreaId: 'space-private-dining',
position: { x: 30, y: 12, z: 30 }
},
{
id: 'poi-service-space',
name: '服务空间',
type: 'service_space',
floorId: 'L1',
position: { x: 31, y: 12, z: 31 }
}
]),
getFloorBusinessPois: vi.fn().mockResolvedValue([]),
getFloorSpaces: vi.fn().mockResolvedValue([
{
id: 'space-private-dining',
name: '西侧餐饮区',
type: 'restaurant',
floorId: 'L1',
visitorVisible: false,
center: { x: 30, y: 12, z: 30 }
},
{
id: 'space-private-cinema',
name: '私享影院',
type: 'theater',
floorId: 'L1',
visitorVisible: false,
center: { x: 32, y: 12, z: 32 }
},
{
id: 'space-route-node',
name: '导航区域',
type: 'navigable_place',
floorId: 'L1',
center: { x: 33, y: 12, z: 33 }
}
]),
getNavigablePlaces: vi.fn().mockResolvedValue([])
})
const repository = new SgsSdkGuideRepository(provider)
const modelRepository = new SgsSdkGuideModelRepository(provider, repository)
const [defaultResults, dining, cinema, privateKeyword, serviceKeyword, structuralKeyword, renderPois] = await Promise.all([
repository.listDestinationPois('L1'),
repository.listQuickFindPois('dining', 'L1'),
repository.listQuickFindPois('cinema', 'L1'),
repository.searchPois('西侧餐饮区', 'L1'),
repository.searchPois('服务空间', 'L1'),
repository.searchPois('导航区域', 'L1'),
modelRepository.loadFloorPois('L1')
])
expect(defaultResults).toEqual([])
expect(dining).toEqual([])
expect(cinema).toEqual([])
expect(privateKeyword).toEqual([])
expect(serviceKeyword).toEqual([])
expect(structuralKeyword).toEqual([])
expect(renderPois).toEqual([])
})
it('merges source-place duplicates before returning canonical visitor IDs', () => {
const basePoi = {
floorId: 'L1',
floorLabel: '1F',
primaryCategory: { id: 'space_restaurant', label: '餐饮', iconType: 'restaurant' },
categories: [{ id: 'space_restaurant', label: '餐饮', iconType: 'restaurant' }],
positionGltf: [40, 12, 40] as [number, number, number],
accessible: false,
kind: 'facility' as const,
sourcePlaceId: 'place-cafe'
}
const results = canonicalizeVisitorSearchPois([
{ ...basePoi, id: 'poi-cafe-a', name: '中庭咖啡' },
{
...basePoi,
id: 'poi-cafe-b',
name: '中庭咖啡取餐台',
positionGltf: [41, 12, 41]
}
])
expect(results.map((poi) => poi.id)).toEqual(['poi-cafe-a'])
})
it('deduplicates co-located sources when they do not provide a shared spatial area ID', async () => {
const repository = new SgsSdkGuideRepository(createSgsProvider({
getFloorPois: vi.fn().mockResolvedValue([{
@@ -348,6 +483,24 @@ describe('GuideRepository visitor search contracts', () => {
primaryCategoryZh: '基础服务设施',
iconType: 'restroom',
positionGltf: position(4)
},
{
id: 'static-vip-reception',
name: '贵宾接待区',
floorId: 'L1',
primaryCategory: 'basic_service_facility',
primaryCategoryZh: '基础服务设施',
iconType: 'restroom',
positionGltf: position(5)
},
{
id: 'static-vip-restroom',
name: '贵宾卫生间',
floorId: 'L1',
primaryCategory: 'basic_service_facility',
primaryCategoryZh: '基础服务设施',
iconType: 'restroom',
positionGltf: position(6)
}
])
} as unknown as StaticNavAssetsProvider
@@ -362,6 +515,10 @@ describe('GuideRepository visitor search contracts', () => {
await expect(repository.searchPois('洗手间', 'L1')).resolves.toMatchObject([
{ id: 'static-restroom' }
])
await expect(repository.searchPois('茶水间', 'L1')).resolves.toEqual([])
await expect(repository.searchPois('贵宾', 'L1')).resolves.toEqual([])
await expect(repository.getPoiById('static-tea-room')).resolves.toBeNull()
await expect(repository.getPoiById('static-vip-restroom')).resolves.toBeNull()
await expect(repository.searchPois('服务', 'L1')).resolves.toEqual([
expect.objectContaining({ id: 'static-service-desk' })
])
@@ -397,6 +554,24 @@ describe('GuideRepository visitor search contracts', () => {
primaryCategoryZh: '讲解点',
iconType: 'guide',
positionGltf: position(2)
},
{
id: 'static-private-tea',
name: '茶水间',
floorId: 'L1',
primaryCategory: 'basic_service_facility',
primaryCategoryZh: '基础服务设施',
iconType: 'restroom',
positionGltf: position(3)
},
{
id: 'static-vip-restroom',
name: '贵宾卫生间',
floorId: 'L1',
primaryCategory: 'basic_service_facility',
primaryCategoryZh: '基础服务设施',
iconType: 'restroom',
positionGltf: position(4)
}
])
} as unknown as StaticNavAssetsProvider

View File

@@ -0,0 +1,86 @@
import { createRequire } from 'node:module'
import { describe, expect, it, vi } from 'vitest'
const require = createRequire(import.meta.url)
const {
isTextBuildFile,
resolveH5BuildPolicy,
tencentMapKeyPlaceholder
} = require('../../scripts/finalize-h5-build-policy.cjs') as {
isTextBuildFile: (filePath: string) => boolean
resolveH5BuildPolicy: (options: {
args?: string[]
projectRoot: string
environment?: Record<string, string | undefined>
loadProductionEnv?: (root: string) => Record<string, string | undefined>
}) => {
allowPlaceholder: boolean
requireTencentMapKey: boolean
tencentMapKey: string
}
tencentMapKeyPlaceholder: string
}
const projectRoot = 'C:/museum-guide'
const validTencentMapKey = 'ABCDE-FGHIJ-KLMNO-PQRST-UVWXY-12345'
describe('finalize H5 build policy', () => {
it('keeps test builds isolated from production map credentials', () => {
const loadProductionEnv = vi.fn(() => ({
VITE_TENCENT_MAP_KEY: validTencentMapKey
}))
expect(resolveH5BuildPolicy({
args: ['--allow-placeholder'],
projectRoot,
environment: { VITE_TENCENT_MAP_KEY: validTencentMapKey },
loadProductionEnv
})).toEqual({
allowPlaceholder: true,
requireTencentMapKey: false,
tencentMapKey: ''
})
expect(loadProductionEnv).not.toHaveBeenCalled()
})
it('loads a well-formed production key only for the production gate', () => {
const loadProductionEnv = vi.fn(() => ({
VITE_TENCENT_MAP_KEY: validTencentMapKey
}))
expect(resolveH5BuildPolicy({
args: ['--require-tencent-map-key'],
projectRoot,
environment: {},
loadProductionEnv
})).toEqual({
allowPlaceholder: false,
requireTencentMapKey: true,
tencentMapKey: validTencentMapKey
})
expect(loadProductionEnv).toHaveBeenCalledWith(projectRoot)
})
it('rejects conflicting flags, placeholders, and malformed production keys', () => {
expect(() => resolveH5BuildPolicy({
args: ['--allow-placeholder', '--require-tencent-map-key'],
projectRoot,
environment: {}
})).toThrow('cannot require a Tencent map key')
for (const key of ['', tencentMapKeyPlaceholder, 'fake-key']) {
expect(() => resolveH5BuildPolicy({
args: ['--require-tencent-map-key'],
projectRoot,
environment: { VITE_TENCENT_MAP_KEY: key },
loadProductionEnv: () => ({})
})).toThrow('well-formed non-placeholder')
}
})
it('scans source maps as text build artifacts', () => {
expect(isTextBuildFile('assets/index.js.map')).toBe(true)
expect(isTextBuildFile('assets/index.js')).toBe(true)
expect(isTextBuildFile('assets/model.glb')).toBe(false)
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { toMuseumPoi } from '@/data/adapters/navAssetsAdapter'
import { resolvePoiCategory } from '@/domain/poiCategories'
import { isVisitorSearchPoi, resolvePoiCategory } from '@/domain/poiCategories'
import type { StaticNavPoiPayload } from '@/data/providers/staticNavAssetsProvider'
const createStaticPoi = (name: string, iconType = 'elevator'): StaticNavPoiPayload => ({
@@ -52,10 +52,23 @@ describe('static nav POI adapter', () => {
const serviceDesk = toMuseumPoi(createStaticPoi('服务台', 'restroom'))
const locker = toMuseumPoi(createStaticPoi('存包处', 'restroom'))
const teaRoom = toMuseumPoi(createStaticPoi('茶水间', 'restroom'))
const vipReception = toMuseumPoi(createStaticPoi('贵宾接待区', 'restroom'))
const vipRestroom = toMuseumPoi(createStaticPoi('贵宾卫生间', 'restroom'))
expect(resolvePoiCategory(serviceDesk)?.id).toBe('service-center')
expect(resolvePoiCategory(locker)?.id).toBe('service-center')
expect(resolvePoiCategory(teaRoom)).toBeNull()
expect(teaRoom.visitorVisible).toBe(false)
expect(vipReception.visitorVisible).toBe(false)
expect(vipRestroom.visitorVisible).toBe(false)
expect(toMuseumPoi({
...createStaticPoi('茶水间', 'restroom'),
visitorVisible: true
}).visitorVisible).toBe(true)
expect(toMuseumPoi({
...createStaticPoi('贵宾卫生间', 'restroom'),
visitorVisible: true
}).visitorVisible).toBe(true)
})
it('splits generic touring fallback records into their visitor-facing semantic kinds', () => {
@@ -73,4 +86,19 @@ describe('static nav POI adapter', () => {
})
expect(resolvePoiCategory(ticketOffice)?.id).toBe('ticket-office')
})
it('keeps a hall visible when its secondary category describes an entrance', () => {
const hall = toMuseumPoi(createTouringPoi('地球展厅'))
hall.kind = 'hall'
hall.categories = [
hall.primaryCategory,
{
id: 'exhibition_hall_entrance',
label: '展厅出入口',
iconType: 'hall_entrance'
}
]
expect(isVisitorSearchPoi(hall)).toBe(true)
})
})