// // LocationReportService.swift // suixinkan // import Foundation /// 位置上报成功结果。 struct LocationReportSuccessResult: Equatable { let reportTimeText: String let locationInfoText: String let shouldShowSuccessDialog: Bool } /// 位置上报共享业务逻辑,对齐 Android `HomeViewModel.reportLocation`。 final class LocationReportService { static let operationIntervalMillis: Int64 = 30_000 private(set) var lastOnlineStatusSwitchTime: Int64 = 0 private(set) var lastLocationReportTime: Int64 = 0 private(set) var isReporting = false private let appStore: AppStore private let locationStateStore: HomeLocationStateStore private let locationProvider: any LocationProviding init( appStore: AppStore = .shared, locationStateStore: HomeLocationStateStore, locationProvider: any LocationProviding = LocationProvider.shared ) { self.appStore = appStore self.locationStateStore = locationStateStore self.locationProvider = locationProvider } /// 切换在线/离线并触发位置上报。 func switchOnlineStatus(api: HomeAPI) async throws -> LocationReportSuccessResult? { let now = currentTimestampMillis() if now - lastOnlineStatusSwitchTime < Self.operationIntervalMillis { let remaining = Int((Self.operationIntervalMillis - (now - lastOnlineStatusSwitchTime)) / 1000) throw LocationReportServiceError.tooFrequent(remainingSeconds: remaining) } guard appStore.currentScenicId > 0 else { throw LocationReportServiceError.missingScenic } let goingOnline = !locationStateStore.isOnline if goingOnline { _ = try await locationProvider.requestSnapshot() locationStateStore.updateOnlineStatus(true) lastOnlineStatusSwitchTime = now return try await reportWithCoordinates( api: api, latitude: nil, longitude: nil, address: nil, type: 1, showSuccessToast: true ) } locationStateStore.updateOnlineStatus(false) lastOnlineStatusSwitchTime = now _ = try await reportWithCoordinates( api: api, latitude: nil, longitude: nil, address: nil, type: 3, showSuccessToast: false ) return nil } /// 手动 GPS 上报。 func manualReport(api: HomeAPI) async throws -> LocationReportSuccessResult { let now = currentTimestampMillis() if now - lastLocationReportTime < Self.operationIntervalMillis { let remaining = Int((Self.operationIntervalMillis - (now - lastLocationReportTime)) / 1000) throw LocationReportServiceError.tooFrequent(remainingSeconds: remaining) } return try await reportWithCoordinates( api: api, latitude: nil, longitude: nil, address: nil, type: 1, showSuccessToast: true ) } /// 指定坐标上报(含标记上报 type=2)。 func reportWithCoordinates( api: HomeAPI, latitude: Double?, longitude: Double?, address: String?, type: Int, showSuccessToast: Bool = true ) async throws -> LocationReportSuccessResult { if type != 3 { let now = currentTimestampMillis() if now - lastLocationReportTime < Self.operationIntervalMillis { let remaining = Int((Self.operationIntervalMillis - (now - lastLocationReportTime)) / 1000) throw LocationReportServiceError.tooFrequent(remainingSeconds: remaining) } } guard appStore.currentScenicId > 0 else { throw LocationReportServiceError.missingScenic } let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !staffId.isEmpty else { throw LocationReportServiceError.missingStaffId } isReporting = true defer { isReporting = false } let snapshot: HomeLocationSnapshot if let latitude, let longitude { let resolvedAddress: String if let address, !address.isEmpty { resolvedAddress = address } else { resolvedAddress = await locationProvider.reverseGeocode( latitude: latitude, longitude: longitude ) } snapshot = HomeLocationSnapshot( latitude: latitude, longitude: longitude, address: resolvedAddress ) } else { snapshot = try await locationProvider.requestSnapshot() } let locationText = snapshot.address.isEmpty ? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude) : snapshot.address _ = try await api.reportLocation( staffId: staffId, latitude: snapshot.latitude, longitude: snapshot.longitude, address: snapshot.address, type: type, scenicId: String(appStore.currentScenicId) ) lastLocationReportTime = currentTimestampMillis() if type != 3 { locationStateStore.startLocationReport() let reportTime = formattedReportTime() if showSuccessToast { // Toast handled by caller via onShowMessage if needed } return LocationReportSuccessResult( reportTimeText: reportTime, locationInfoText: locationText, shouldShowSuccessDialog: true ) } return LocationReportSuccessResult( reportTimeText: "", locationInfoText: locationText, shouldShowSuccessDialog: false ) } private func formattedReportTime() -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "HH:mm" return formatter.string(from: Date()) } private func currentTimestampMillis() -> Int64 { Int64(Date().timeIntervalSince1970 * 1000) } } /// 位置上报业务错误。 enum LocationReportServiceError: LocalizedError, Equatable { case tooFrequent(remainingSeconds: Int) case missingScenic case missingStaffId case locating var errorDescription: String? { switch self { case .tooFrequent(let remaining): "操作过于频繁,请\(remaining)秒后再试" case .missingScenic: "请先选择景区" case .missingStaffId: "获取用户ID失败" case .locating: "正在定位中,请稍候" } } }