Files
suixinkan_ios_new/suixinkan/Features/OperatingArea/Models/OperatingAreaModels.swift
汉秋 a04168cf30 新增运营区域与飞手认证模块,并完善直播推流就绪流程
将运营区域与飞手认证从首页占位页迁移为完整模块,扩展 Live 播放与推流就绪流程,并新增飞手证书 OSS 上传。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 18:15:59 +08:00

375 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OperatingAreaModels.swift
// suixinkan
//
// Created by Codex on 2026/6/25.
//
import Foundation
///
enum OperatingAreaEntryMode: Equatable {
case storeAdmin(storeId: Int)
case scenicAdmin(scenicId: Int)
var title: String {
switch self {
case .storeAdmin:
"店铺运营区域"
case .scenicAdmin:
"景区运营区域"
}
}
}
///
struct OperatingAreaItem: Decodable, Equatable, Identifiable {
let id: Int
let name: String
let businessMapArea: OperatingMapArea?
let statusText: String
let typeText: String
let auditStatusText: String
enum CodingKeys: String, CodingKey {
case id
case name
case businessMapArea = "business_map_area"
case statusText = "status_text"
case typeText = "type_text"
case auditStatusText = "audit_status_text"
}
init(
id: Int = 0,
name: String = "",
businessMapArea: OperatingMapArea? = nil,
statusText: String = "",
typeText: String = "",
auditStatusText: String = ""
) {
self.id = id
self.name = name
self.businessMapArea = businessMapArea
self.statusText = statusText
self.typeText = typeText
self.auditStatusText = auditStatusText
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.operatingDecodeLossyInt(forKey: .id) ?? 0
name = try container.operatingDecodeLossyString(forKey: .name)
businessMapArea = try container.decodeIfPresent(OperatingMapArea.self, forKey: .businessMapArea)
statusText = try container.operatingDecodeLossyString(forKey: .statusText)
typeText = try container.operatingDecodeLossyString(forKey: .typeText)
auditStatusText = try container.operatingDecodeLossyString(forKey: .auditStatusText)
}
}
/// JSON `business_map_area`
enum OperatingMapArea: Decodable, Equatable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([OperatingMapArea])
case object([String: OperatingMapArea])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode([OperatingMapArea].self) {
self = .array(value)
} else {
let keyed = try decoder.container(keyedBy: OperatingDynamicCodingKey.self)
var object: [String: OperatingMapArea] = [:]
for key in keyed.allKeys {
object[key.stringValue] = try keyed.decode(OperatingMapArea.self, forKey: key)
}
self = .object(object)
}
}
/// JSON JSON
static func parseString(_ text: String) -> OperatingMapArea? {
guard let data = text.data(using: .utf8),
let raw = try? JSONSerialization.jsonObject(with: data)
else { return nil }
return fromJSONObject(raw)
}
private static func fromJSONObject(_ value: Any) -> OperatingMapArea {
switch value {
case is NSNull:
return .null
case let value as Bool:
return .bool(value)
case let value as NSNumber:
return .number(value.doubleValue)
case let value as String:
return .string(value)
case let value as [Any]:
return .array(value.map(fromJSONObject))
case let value as [String: Any]:
return .object(value.mapValues(fromJSONObject))
default:
return .null
}
}
}
///
struct OperatingGeoPoint: Equatable, Hashable {
let latitude: Double
let longitude: Double
}
///
struct OperatingFenceRing: Identifiable, Equatable {
let id: String
let itemId: Int
let regionName: String
let points: [OperatingGeoPoint]
let isCurrentStore: Bool
init(itemId: Int, regionName: String, points: [OperatingGeoPoint], isCurrentStore: Bool, ringIndex: Int) {
self.id = "\(itemId)-\(ringIndex)"
self.itemId = itemId
self.regionName = regionName
self.points = points
self.isCurrentStore = isCurrentStore
}
}
///
enum OperatingAreaBlockReason: Equatable {
case missingStore
case missingScenic
case unsupportedRole
case emptyAreaList
case noParsableFenceData
case backendMessage(String)
var message: String {
switch self {
case .missingStore:
"请先选择店铺"
case .missingScenic:
"请先选择景区"
case .unsupportedRole:
"当前账号暂不支持运营区域"
case .emptyAreaList:
"当前景区无区域数据"
case .noParsableFenceData:
"有区域记录,但未解析出有效围栏,请检查坐标数据"
case .backendMessage(let message):
message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "运营区域加载失败" : message
}
}
}
/// GeoJSON JSON
enum OperatingAreaParser {
/// `business_map_area`
static func parseToRings(_ area: OperatingMapArea?) -> [[OperatingGeoPoint]] {
guard let area else { return [] }
return parseElement(area)
}
private static func parseElement(_ value: OperatingMapArea) -> [[OperatingGeoPoint]] {
switch value {
case .null, .bool, .number:
return []
case .string(let text):
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let parsed = OperatingMapArea.parseString(trimmed) else { return [] }
return parseElement(parsed)
case .array(let array):
return parseArrayRoot(array)
case .object(let object):
return parseObject(object)
}
}
private static func parseObject(_ object: [String: OperatingMapArea]) -> [[OperatingGeoPoint]] {
let type: String?
if case let .string(rawType)? = object["type"] {
type = rawType.lowercased()
} else {
type = nil
}
switch type {
case "polygon":
guard case let .array(coordinates)? = object["coordinates"],
case let .array(outer)? = coordinates.first
else { return [] }
return parseRing(outer).map { [$0] } ?? []
case "multipolygon":
guard case let .array(polygons)? = object["coordinates"] else { return [] }
return polygons.compactMap { polygon -> [OperatingGeoPoint]? in
guard case let .array(rings) = polygon,
case let .array(firstRing)? = rings.first
else { return nil }
return parseRing(firstRing)
}
default:
for key in ["path", "paths", "points", "coordinates"] {
if let nested = object[key] {
return parseElement(nested)
}
}
return []
}
}
private static func parseArrayRoot(_ array: [OperatingMapArea]) -> [[OperatingGeoPoint]] {
guard let first = array.first else { return [] }
switch first {
case .object:
return parseObjectRing(array).map { [$0] } ?? []
case .array(let nested):
guard let sample = nested.first else { return [] }
switch sample {
case .array(let pointCandidate):
if pointCandidate.count >= 2, pointCandidate[0].numberValue != nil, pointCandidate[1].numberValue != nil {
return parseRing(nested).map { [$0] } ?? []
}
return array.compactMap { element -> [OperatingGeoPoint]? in
guard case let .array(segment) = element else { return nil }
return parseRing(segment)
}
case .object:
return parseObjectRing(nested).map { [$0] } ?? []
default:
return parseRing(nested).map { [$0] } ?? []
}
default:
return []
}
}
private static func parseObjectRing(_ array: [OperatingMapArea]) -> [OperatingGeoPoint]? {
let points = array.compactMap { element -> OperatingGeoPoint? in
guard case let .object(object) = element else { return nil }
return pointFromObject(object)
}
return points.count >= 3 ? points : nil
}
private static func parseRing(_ array: [OperatingMapArea]) -> [OperatingGeoPoint]? {
let points = array.compactMap { element -> OperatingGeoPoint? in
switch element {
case .array(let pair):
guard pair.count >= 2,
let a = pair[0].numberValue,
let b = pair[1].numberValue
else { return nil }
return normalizePair(a, b)
case .object(let object):
return pointFromObject(object)
default:
return nil
}
}
return points.count >= 3 ? points : nil
}
private static func pointFromObject(_ object: [String: OperatingMapArea]) -> OperatingGeoPoint? {
let lat = object["lat"]?.numberValue ?? object["latitude"]?.numberValue
let lng = object["lng"]?.numberValue ?? object["lon"]?.numberValue ?? object["longitude"]?.numberValue
guard let lat, let lng else { return nil }
return OperatingGeoPoint(latitude: lat, longitude: lng)
}
private static func normalizePair(_ a: Double, _ b: Double) -> OperatingGeoPoint {
if looksLikeLngLatPair(lng: a, lat: b) {
return OperatingGeoPoint(latitude: b, longitude: a)
}
if looksLikeLatLngPair(lat: a, lng: b) {
return OperatingGeoPoint(latitude: a, longitude: b)
}
if abs(a) > 90 {
return OperatingGeoPoint(latitude: b, longitude: a)
}
return OperatingGeoPoint(latitude: a, longitude: b)
}
private static func looksLikeLngLatPair(lng: Double, lat: Double) -> Bool {
(-90.0...90.0).contains(lat) && abs(lng) <= 180.0 && abs(lng) > abs(lat)
}
private static func looksLikeLatLngPair(lat: Double, lng: Double) -> Bool {
(-90.0...90.0).contains(lat) && abs(lng) <= 180.0 && abs(lat) <= abs(lng)
}
}
private struct OperatingDynamicCodingKey: CodingKey {
let stringValue: String
let intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}
init?(intValue: Int) {
stringValue = String(intValue)
self.intValue = intValue
}
}
private extension OperatingMapArea {
var numberValue: Double? {
switch self {
case .number(let value):
return value
case .string(let text):
return Double(text.trimmingCharacters(in: .whitespacesAndNewlines))
default:
return nil
}
}
}
private extension KeyedDecodingContainer {
func operatingDecodeLossyString(forKey key: Key) throws -> String {
if let value = try? decodeIfPresent(String.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return String(value)
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return String(value)
}
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value ? "true" : "false"
}
return ""
}
func operatingDecodeLossyInt(forKey key: Key) throws -> Int? {
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
if let intValue = Int(text) { return intValue }
if let doubleValue = Double(text) { return Int(doubleValue) }
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return Int(value)
}
return nil
}
}