将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
7.1 KiB
Swift
212 lines
7.1 KiB
Swift
//
|
|
// LocationReportService.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import CoreLocation
|
|
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
|
|
static let manualReportDesiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyHundredMeters
|
|
|
|
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.session.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,
|
|
desiredAccuracy: Self.manualReportDesiredAccuracy
|
|
)
|
|
}
|
|
|
|
/// 指定坐标上报(含标记上报 type=2)。
|
|
func reportWithCoordinates(
|
|
api: HomeAPI,
|
|
latitude: Double?,
|
|
longitude: Double?,
|
|
address: String?,
|
|
type: Int,
|
|
showSuccessToast: Bool = true,
|
|
desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
|
|
) 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.session.currentScenicId > 0 else {
|
|
throw LocationReportServiceError.missingScenic
|
|
}
|
|
let staffId = appStore.session.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(desiredAccuracy: desiredAccuracy)
|
|
}
|
|
|
|
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.session.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:
|
|
"正在定位中,请稍候"
|
|
}
|
|
}
|
|
}
|