// // StartLocationHandler.swift // ylgamehall // // H5 → Native handler:startlocation — 定位(Phase 5 接入高德 SDK 后激活真实路径) // 契约:docs/H5-Native-Contract.md §3.1 [20] + §3.2 [6] // // 完整链路: // 1. LocationService.shared.requestOnce()(actor,包 AMapLocationManager 异步) // 2. bridge.call("getlocationinfo", 9 字段) 反向 callback // - latitude / longitude 是 **string**(`String(format: "%f")`) // - province 小写 p(与 sharelogin.Province 大写不同) // 3. 失败 → getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"}) // // ⚠️ canImport(AMapLocationKit) 守卫:SDK 未链接时 LocationService 抛 sdkNotLinked, // 此处兜底走 stub(与 Phase 2 一致),仅响应 callback 不触发反向 getlocationinfo。 // import Foundation import os.log // 工程默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor,全局 let 也会被 MainActor 隔离; // handler 闭包是 @Sendable 非隔离上下文,故显式 nonisolated(Logger 本身 Sendable,安全)。 private nonisolated let startLocLog = Logger(subsystem: "ylgamehall", category: "Location") public enum StartLocationHandler { /// - Parameters: /// - label: 容器标识(`lobby` / `subGame`),仅用于诊断日志。 /// - location: **本容器私有**的 LocationService。msext 大厅 / 子游戏各持一个 /// `AMapLocationManager`,不可共用(见 LocationService.init 注释)。 public static func register(on bridge: any BridgeProtocol, label: String = "?", location: LocationService) { bridge.register("startlocation") { data, callback in // 与 msext 行为一致:先回 cb 字面,不等定位完成 callback?(.string("startlocation")) diagLog("[\(label)] ← startlocation 入参 type=\(data?.asLooseString ?? "nil")") #if canImport(AMapLocationKit) // msext NewRootVC.m:411-421 / gameController.m:533-543 的分支逐字等价: // int tempinfo = [data intValue]; // if (tempinfo == 1) [self.locationManager startUpdatingLocation]; // 持续 // else [self reGeocodeAction]; // 单次 // 现网两个 H5 包(gamehall / jinxianmahjong)实测入参恒为 2,持续定位分支 // 是死代码;但 msext 有、我们缺就是原生侧的行为不一致,按 msext 补齐。 let isContinuous = Int(data?.asLooseString ?? "") == 1 Task { @MainActor in // 单次路径下一次请求可能产出两条 outcome(msext completionBlock 的原始 // 控制流);持续路径下每次定位更新都会回一条 .success。 let dispatch: @MainActor (LocationOutcome) -> Void = { outcome in switch outcome { case .failure: // msext gameController.m:2507 等价 diagLog("[\(label)] → getlocationinfo errorCode=12") bridge.call("getlocationinfo", data: .object([ "errorCode": .number(12), "errorMsg": .string("缺少定位权限") ]), callback: nil) case .success(let payload): // msext gameController.m:2528 的 9 字段 + `errorCode: 0`。 // // ⚠️ 契约影响(docs/H5-Native-Contract.md §3.2 表 B): // msext 全工程只在**失败**分支发 `errorCode: 12`,成功分支 // 9 个字段里**没有** errorCode —— 这是原工程的遗漏。 // H5 侧以 `errorCode == 0` 作为"定位数据有效"的判据: // 子游戏 jinxianmahjong 的 05_Func.js(2026-07-19)在 // `Func.startlocation` / `Func.getlocation` 的兜底桩里 // 自造的定位对象就带 `"errorCode":0`;而大厅 gamehall 的 // 05_Func.js(2026-02-04)整份文件都没有 errorCode。 // 这正是"大厅定位正常、子游戏拿不到"的原生侧成因:成功包缺 // errorCode 时子游戏 H5 不认这份数据。 // 按项目方决定补齐(此处是**有意偏离** msext 的一处,其余 // 定位链路仍严格对齐)。 diagLog("[\(label)] → getlocationinfo 成功 city=\(payload.city) province=\(payload.province)") bridge.call("getlocationinfo", data: .object([ "errorCode": .number(0), "address": .string(payload.address), "city": .string(payload.city), "cityCode": .string(payload.cityCode), "country": .string(payload.country), "district": .string(payload.district), "latitude": .decimal(payload.latitude), // ← 数字(非字符串),6 位小数 "longitude": .decimal(payload.longitude), // ← 数字(非字符串),6 位小数 "province": .string(payload.province), // ← 小写 p "street": .string(payload.street) ]), callback: nil) } } if isContinuous { location.startContinuous(caller: label, emit: dispatch) } else { location.requestOnce(caller: label, emit: dispatch) } } #endif } } }