第38篇定位权限同意了仍显示旧位置用位置快照和过期策略收口摘要定位问题经常被误判成权限问题。用户同意权限后页面仍显示旧城市可能是缓存没过期、刷新失败被旧值覆盖、页面直接读了上次位置。更稳的做法是把权限状态、位置快照、过期策略和刷新结果分开让页面明确展示当前位置、旧位置或不可用状态。一个门店页曾经出现“我人在上海页面还显示杭州”的问题。权限弹窗已经同意接口也没有报错但页面一直拿上次缓存的位置。根因不是定位能力本身而是页面把 lastLocation 当成 currentLocation 用刷新失败时又静默回退用户完全不知道自己看到的是旧位置。这篇文章解决四个实际问题把权限状态、定位中、可用位置、旧位置、失败状态拆开。给位置快照加capturedAt和过期时间不让旧值冒充新值。刷新失败时保留旧位置但明确标记不静默覆盖。用前后台、城市切换、断网三条路径验证。先问页面展示的是新位置还是旧快照定位同意后仍然显示旧位置时先不要重装应用。要看页面当前展示的数据来源是实时刷新结果还是上一次成功定位的快照。位置现象风险处理方向权限已同意页面仍显示旧城市缓存没有过期判断给快照加 TTL刷新失败静默沿用旧坐标用户误以为是当前位置标记 stale前后台切换回前台不刷新门店列表过期onShown 触发条件刷新手动选城市自动定位覆盖手选来源优先级混乱区分 manual 和 gps这一步的价值是先把问题放回运行链路。只要能确认问题停在哪个位置后面就不用靠猜测改页面。权限、快照和展示态不是一回事权限只是允许你尝试获取位置不代表当前坐标可用。页面应该消费 LocationViewState而不是直接读缓存坐标。模块应该负责不应该负责PermissionGate授权、拒绝、仅本次城市名称SnapshotStore经纬度、来源、时间页面文案RefreshService刷新位置并判断过期按钮布局NearbyPage可用、旧值、失败展示底层定位实现CitySelector手动城市优先级权限弹窗细节边界定清后代码就不会在页面、服务和回调之间来回复制同一段判断。后续新增场景也能先判断应该落在哪一层。位置快照必须带来源和时间exportenumLocationSource{Gpsgps,Manualmanual,Cachedcached}exportinterfaceLocationSnapshot{latitude:numberlongitude:numbercityName:stringsource:LocationSource capturedAt:number}exportfunctionisLocationExpired(snapshot:LocationSnapshot,ttlMs:number):boolean{returnDate.now()-snapshot.capturedAtttlMs}source决定页面该怎么解释当前位置capturedAt决定能不能继续使用。没有这两个字段旧坐标很容易被当成实时定位。这类模型最好放在models或common中页面、Service 和仓储都使用同一份类型避免各层用字符串互相猜。服务层返回可展示状态exportenumLocationViewKind{Freshfresh,Stalestale,PermissionDeniedpermission_denied,Unavailableunavailable}exportinterfaceLocationViewState{kind:LocationViewKind snapshot:LocationSnapshot|nullmessage:string}exportclassLocationRefreshService{constructor(privatereadonlystore:LocationSnapshotStore,privatereadonlylocator:DeviceLocator){}asyncrefresh():PromiseLocationViewState{constlastawaitthis.store.load()constnextawaitthis.locator.locate().catch(()null)if(next!null){awaitthis.store.save(next)return{kind:LocationViewKind.Fresh,snapshot:next,message:}}if(last!null){return{kind:LocationViewKind.Stale,snapshot:last,message:定位失败已显示上次位置}}return{kind:LocationViewKind.Unavailable,snapshot:null,message:暂时无法获取位置}}}失败时不是直接返回旧坐标而是返回Stale。页面可以继续展示门店但必须告诉用户这不是刚刚定位的结果。Service 的目标不是把所有逻辑都塞进去而是把跨页面、跨生命周期或需要持久化的判断集中起来。页面只表达用户动作。页面按状态显示不直接拼坐标Componentstruct NearbyStorePage{StatelocationState:LocationViewState{kind:LocationViewKind.Unavailable,snapshot:null,message:正在获取位置}privaterefreshService:LocationRefreshServicecreateLocationRefreshService()asyncaboutToAppear():Promisevoid{this.locationStateawaitthis.refreshService.refresh()}build(){Column({space:12}){LocationBanner({state:this.locationState,refreshTap:()this.refreshNow()})StoreList({cityName:this.locationState.snapshot?.cityName??})}}}页面拿到的是可展示状态。LocationBanner可以根据 Fresh、Stale、Unavailable 展示不同文案不再把旧位置伪装成当前位置。页面层要控制展示和交互节奏但不应该拥有底层事实。这样页面重建、横竖屏变化或返回前台时都能重新从 Service 拿到可信结果。回前台只在过期时刷新exportclassLocationResumePolicy{constructor(privatereadonlystore:LocationSnapshotStore){}asyncshouldRefresh(ttlMs:number):Promiseboolean{constsnapshotawaitthis.store.load()if(snapshotnull){returntrue}if(snapshot.sourceLocationSource.Manual){returnfalse}returnisLocationExpired(snapshot,ttlMs)}}这段策略避免两个问题不刷新导致旧值长期存在每次回前台都刷新又覆盖用户手动选择的城市。实际项目里很多问题不是第一次进入页面暴露而是在冷启动、返回前台、页面重建或回调晚到时暴露。把这段生命周期补上修复才算闭环。降级显示要把来源说清楚情况页面表现工程处理权限拒绝显示手动选择入口不要反复弹权限说明定位失败但有旧值展示旧位置标签允许手动刷新无任何快照展示空结果和城市选择不要显示默认城市假象用户手动选城优先手动来源自动定位不覆盖兜底路径不是可有可无。用户遇到异常时页面至少要给出当前状态、可执行动作和可排查线索。排查时搜旧坐标直读点rg-nlastLocation|currentLocation|latitude|longitude|cityNameentry features common rg-ncapturedAt|LocationSource|LocationViewKind|Staleentry features common rg-naboutToAppear|onShown|onPageShow|Permissionentry features common第一组命令找旧路径第二组命令找新边界第三组命令看生命周期或回调位置。命中后不要只看字段名要判断它是否仍在直接改页面状态。验证清单验证项预期结果首次授权成功页面显示新城市和刷新时间断网刷新失败保留旧位置并标记旧值快照过期回前台触发刷新手动选城市自动定位不覆盖手选结果拒绝权限出现手动选择入口这些验证最好在真机或模拟器里按顺序走一遍。只看源码很难发现冷启动、重启和回调晚到这类问题。常见问题和处理方式现象常见原因处理方式同意权限仍旧城市页面直接读 lastLocation改为消费 LocationViewState刷新失败无提示旧值静默回退返回 Stale 状态手选被覆盖来源优先级缺失Manual 来源优先回前台频繁跳城市无 TTL 策略只在过期时刷新如果现象没有出现在表里仍然建议按“输入来源 - 中间状态 - 持久化或页面展示 - 失败兜底”的顺序排查。小结位置不是一个字符串而是一份有来源的快照定位问题要把权限、快照、刷新和展示拆开。权限同意只表示可以尝试定位真正交给页面的应该是带来源、时间和状态的 LocationViewState。这样旧位置不会冒充当前位置失败路径也不会悄悄骗过用户。L 策略 | 只在过期时刷新 |如果现象没有出现在表里仍然建议按“输入来源 - 中间状态 - 持久化或页面展示 - 失败兜底”的顺序排查。小结位置不是一个字符串而是一份有来源的快照定位问题要把权限、快照、刷新和展示拆开。权限同意只表示可以尝试定位真正交给页面的应该是带来源、时间和状态的 LocationViewState。这样旧位置不会冒充当前位置失败路径也不会悄悄骗过用户。