// // WildPhotographerReportMockStore.swift // suixinkan // import CoreLocation import Foundation import MapKit /// 举报摄影师 Mock 数据仓库,统一管理演示种子数据和地图标记构建。 enum WildPhotographerReportMockStore { static let fallbackLocation = "黄山风景区 · 光明顶景区" static let initialImages = [ WildReportAttachment(kind: .image, title: "现场图片1") ] static let initialVideos = [ WildReportAttachment(kind: .video, title: "现场视频1") ] static let seedRiskPoints = [ WildReportRiskPoint( id: 1, title: "光明顶观景台附近", location: "黄山风景区 · 光明顶景区", distance: "120m", riskLevel: "高", evidenceCount: 6, latestTime: "10分钟前", description: "多名游客反馈有疑似未备案摄影师主动揽客,集中在观景台东侧步道附近。" ), WildReportRiskPoint( id: 2, title: "迎客松游客出口", location: "黄山风景区 · 玉屏景区", distance: "860m", riskLevel: "中", evidenceCount: 3, latestTime: "32分钟前", description: "疑似人员以低价加急出片为话术引导游客线下付款,建议现场核查。" ), WildReportRiskPoint( id: 3, title: "云谷索道排队区", location: "黄山风景区 · 云谷景区", distance: "1.4km", riskLevel: "低", evidenceCount: 2, latestTime: "1小时前", description: "排队区附近存在疑似私下拉客行为,已收到图片和位置线索。" ) ] static let seedReports = [ WildReportRecord( id: "JB20260618001", title: "摄影师举报", reportType: WildReportType(value: 2, label: "疑似打野"), status: .processing, location: "那拉提景区 · 空中草原入口附近", submitTime: "2026-06-18 10:24", description: "疑似人员在观景台附近主动向游客售卖拍摄服务,并引导线下转账。", wechatID: "wx_photographer_01", phoneNumber: "13812345678", imageCount: 3, videoCount: 1, handlerInfo: nil ), WildReportRecord( id: "JB20260617008", title: "摄影师举报", reportType: WildReportType(value: 2, label: "疑似打野"), status: .processed, location: "那拉提景区 · 河谷草原游客中心", submitTime: "2026-06-17 16:42", description: "现场发现疑似未备案摄影师持续向游客推销旅拍套餐。", wechatID: "nalati_photo", phoneNumber: "", imageCount: 2, videoCount: 1, handlerInfo: processedHandlerInfo( handlerName: "赵强", handlerPhone: "15938267120", processingAt: "2026-06-17 17:10", processedAt: "2026-06-17 18:26", processOpinion: "已到场核查并完成劝离,处理结果已同步景区公安。", imageSeedPrefix: "handler-jb008" ) ), WildReportRecord( id: "JB20260616003", title: "摄影师举报", reportType: WildReportType(value: 3, label: "未戴工牌"), status: .pending, location: "那拉提景区 · 盘龙古道停车区", submitTime: "2026-06-16 09:15", description: "停车区附近有摄影师未佩戴工牌开展拍摄服务,已收到图片和位置线索。", wechatID: "", phoneNumber: "13987654321", imageCount: 1, videoCount: 0, handlerInfo: nil ), WildReportRecord( id: "JB20260615002", title: "摄影师举报", reportType: WildReportType(value: 4, label: "其他"), status: .processed, location: "那拉提景区 · 雪莲谷步道", submitTime: "2026-06-15 14:30", description: "举报线索经核查未发现异常摄影师服务行为,已归档。", wechatID: "grassland_cam", phoneNumber: "13600001111", imageCount: 1, videoCount: 0, handlerInfo: processedHandlerInfo( handlerName: "李娜", handlerPhone: "18674598210", processingAt: "2026-06-15 15:05", processedAt: "2026-06-15 16:55", processOpinion: "现场核查未发现未备案摄影师,已归档处理。", imageSeedPrefix: "handler-jb002", includeVideo: false ) ) ] static let seedSupplementaryEvidences: [String: [WildReportSupplementaryEvidence]] = [ "JB20260618001": [ WildReportSupplementaryEvidence( id: "SE001", submitTime: "2026-06-18 14:20", text: "补充现场视频,对方仍在揽客。", imageCount: 1, videoCount: 1 ) ] ] static let reportDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" formatter.locale = Locale(identifier: "en_US_POSIX") return formatter }() static let submitTimeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm" formatter.locale = Locale(identifier: "en_US_POSIX") return formatter }() static let nalatiCenter = CLLocationCoordinate2D(latitude: 43.382, longitude: 84.032) private static let clueID1 = "2026年6月22日-0001" private static let clueID2 = "2026年6月22日-0002" private static let clueID3 = "2026年6月22日-0003" private static let initialProcessingClueIDs: Set = [clueID2] /// 构建已处理举报的处理人信息 Mock 数据。 static func processedHandlerInfo( handlerName: String, handlerPhone: String, processingAt: String, processedAt: String, processOpinion: String, imageSeedPrefix: String, includeVideo: Bool = true ) -> WildReportHandlerInfo { var media: [WildReportCompletionMediaItem] = [ WildReportCompletionMediaItem( id: "\(imageSeedPrefix)-img-1", kind: .image, coverURL: "https://picsum.photos/seed/\(imageSeedPrefix)-1/640/480", duration: nil ), WildReportCompletionMediaItem( id: "\(imageSeedPrefix)-img-2", kind: .image, coverURL: "https://picsum.photos/seed/\(imageSeedPrefix)-2/640/480", duration: nil ) ] if includeVideo { media.append( WildReportCompletionMediaItem( id: "\(imageSeedPrefix)-vid-1", kind: .video, coverURL: "https://picsum.photos/seed/\(imageSeedPrefix)-video/640/480", duration: "00:18" ) ) } return WildReportHandlerInfo( handlerName: handlerName, handlerPhone: handlerPhone, processingAt: processingAt, processedAt: processedAt, processOpinion: processOpinion, completionMedia: media ) } /// 从已有举报编号中提取最大序号,供新编号递增。 static func maxSequence(from reports: [WildReportRecord]) -> Int { reports.compactMap { record in guard record.id.count >= 3 else { return nil } return Int(record.id.suffix(3)) }.max() ?? 0 } /// 解析联系方式,区分微信号和手机号。 static func parseContact(_ contact: String?) -> (wechat: String, phone: String) { guard let contact else { return ("", "") } let trimmed = contact.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return ("", "") } let digits = trimmed.filter(\.isNumber) if digits.count >= 7, trimmed.filter({ $0.isNumber || "+-() ".contains($0) }).count == trimmed.count { return ("", trimmed) } return (trimmed, "") } /// 根据线索状态返回地图标记类型。 static func markerKind(for clue: WildReportRadarActiveClue) -> WildReportMapMarkerKind { switch clue.type { case .blue: return .selfLocation case .green: return .peerPhotographer case .red: if clue.processingStarted && !clue.completed { return .processingClue } return .wildPhotographer } } /// 构建风险地图全部标记点。 static func buildMarkers(record: WildReportRecord, riskPoints: [WildReportRiskPoint]) -> [WildReportMapMarker] { _ = record _ = riskPoints let blueCoordinate = CLLocationCoordinate2D(latitude: 43.3779, longitude: 84.0217) let redSeeds: [(id: String, name: String, coordinate: CLLocationCoordinate2D, clue: WildReportRadarActiveClue)] = [ redSeed( id: clueID1, name: "空中草原观景道", coordinate: CLLocationCoordinate2D(latitude: 43.4068, longitude: 84.0582), updatedAt: "5分钟前", distanceText: "320米", reporterName: "张三", reporterPhone: "13333333333", contents: [ "多人结伴揽客,疑似无资质摄影团队,携带专业器材在观景道附近活动。", "补充证据:对方仍在向游客推销拍照套餐,并拒绝出示相关资质。" ], imageSeeds: ["nalati-radar-1", "nalati-radar-2", "nalati-radar-3"] ), redSeed( id: clueID2, name: "塔吾萨尼步道", coordinate: CLLocationCoordinate2D(latitude: 43.3652, longitude: 84.0385), updatedAt: "18分钟前", distanceText: "780米", reporterName: "李四", reporterPhone: "18674598210", contents: [ "疑似二次转移,仍在塔吾萨尼步道附近揽客拍照。", "补充:对方穿黑色外套,携带三脚架,在步道入口反复搭讪游客。" ], imageSeeds: ["nalati-radar-4", "nalati-radar-5"] ), redSeed( id: clueID3, name: "河谷草原西南侧", coordinate: CLLocationCoordinate2D(latitude: 43.3546, longitude: 84.0136), updatedAt: "昨天17:45", distanceText: "910米", reporterName: "王五", reporterPhone: "15938267120", contents: ["游客反馈有人在步道旁揽客拍照,行为较为可疑。"], imageSeeds: ["nalati-radar-8"] ) ] let nearestText = nearestAbnormalText( blueCoordinate: blueCoordinate, redCoordinates: redSeeds.map(\.coordinate) ) var markers: [WildReportMapMarker] = [ WildReportMapMarker( id: "ME", title: "河谷草原入口", coordinate: blueCoordinate, kind: .selfLocation, detail: .activeClue( WildReportRadarActiveClue( id: "ME", type: .blue, displayTitle: "当前位置:河谷草原入口", statusText: "当前位置", actionText: "刷新定位", nearestAbnormalText: nearestText, distance: "0 米", direction: "蓝色基准点", updatedAt: "实时定位", name: "河谷草原入口", storeName: nil, avatar: nil, summary: "蓝色标记为当前位置,可对比周边摄影师风险点位距离。", detail: "当前定位在河谷草原入口附近,建议优先关注 800 米内的异常点位。", distanceText: nil, reporterName: nil, reporterPhone: nil, maskedReporterPhone: nil, reportContents: [], images: [], processingStarted: false, completed: false, record: nil, processingTimeline: [], completionPhoto: nil ) ) ) ] for seed in redSeeds { let processingStarted = initialProcessingClueIDs.contains(seed.id) let clue = enrichClue(seed.clue, processingStarted: processingStarted) markers.append( WildReportMapMarker( id: seed.id, title: seed.name, coordinate: seed.coordinate, kind: markerKind(for: clue), detail: .activeClue(clue) ) ) } markers.append(contentsOf: greenMarkers()) return markers } /// 根据全部标记计算地图可视区域。 static func region(for markers: [WildReportMapMarker]) -> MKCoordinateRegion { guard let first = markers.first else { return MKCoordinateRegion( center: nalatiCenter, span: MKCoordinateSpan(latitudeDelta: 0.025, longitudeDelta: 0.025) ) } var minLat = first.coordinate.latitude var maxLat = first.coordinate.latitude var minLon = first.coordinate.longitude var maxLon = first.coordinate.longitude for marker in markers { minLat = min(minLat, marker.coordinate.latitude) maxLat = max(maxLat, marker.coordinate.latitude) minLon = min(minLon, marker.coordinate.longitude) maxLon = max(maxLon, marker.coordinate.longitude) } let center = CLLocationCoordinate2D( latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2 ) let span = MKCoordinateSpan( latitudeDelta: max(0.022, (maxLat - minLat) * 1.8), longitudeDelta: max(0.022, (maxLon - minLon) * 1.8) ) return MKCoordinateRegion(center: center, span: span) } private static func redSeed( id: String, name: String, coordinate: CLLocationCoordinate2D, updatedAt: String, distanceText: String, reporterName: String, reporterPhone: String, contents: [String], imageSeeds: [String] ) -> (id: String, name: String, coordinate: CLLocationCoordinate2D, clue: WildReportRadarActiveClue) { let reportContents = contents.enumerated().map { index, content in WildReportRadarReportContent(time: index == 0 ? "14:32:18" : "14:45:06", content: content) } return ( id, name, coordinate, WildReportRadarActiveClue( id: id, type: .red, displayTitle: "线索 ID:\(id)", statusText: "疑似打野", actionText: "导航到此", nearestAbnormalText: nil, distance: nil, direction: nil, updatedAt: updatedAt, name: name, storeName: nil, avatar: nil, summary: nil, detail: nil, distanceText: distanceText, reporterName: reporterName, reporterPhone: reporterPhone, maskedReporterPhone: maskPhone(reporterPhone), reportContents: reportContents, images: imageSeeds.map { "https://picsum.photos/seed/\($0)/640/480" }, processingStarted: false, completed: false, record: nil, processingTimeline: [], completionPhoto: nil ) ) } private static func greenMarkers() -> [WildReportMapMarker] { let seeds: [(id: String, coordinate: CLLocationCoordinate2D, name: String, store: String, detail: String)] = [ ("PG20240618001", CLLocationCoordinate2D(latitude: 43.4042, longitude: 84.0186), "阿依达娜", "空中草原旅拍店", "当前在空中草原区域服务游客拍摄。"), ("PG20240618002", CLLocationCoordinate2D(latitude: 43.3985, longitude: 84.0408), "波拉提", "天界台摄影服务点", "当前在天界台附近接待预约游客。"), ("PG20240618003", CLLocationCoordinate2D(latitude: 43.3828, longitude: 84.0794), "米热丽", "塔吾萨尼影像馆", "当前在塔吾萨尼步道附近提供旅拍服务。") ] return seeds.map { seed in let clue = WildReportRadarActiveClue( id: seed.id, type: .green, displayTitle: "认证摄影师", statusText: "认证摄影师", actionText: "导航到此", nearestAbnormalText: nil, distance: "430 米", direction: "景区内", updatedAt: "在线", name: seed.name, storeName: seed.store, avatar: "person.crop.circle.fill", summary: "绿色标记为已认证摄影师,可与红色摄影师风险点位区分。", detail: seed.detail, distanceText: nil, reporterName: nil, reporterPhone: nil, maskedReporterPhone: nil, reportContents: [], images: [], processingStarted: false, completed: false, record: nil, processingTimeline: [], completionPhoto: nil ) return WildReportMapMarker( id: seed.id, title: seed.name, coordinate: seed.coordinate, kind: .peerPhotographer, detail: .activeClue(clue) ) } } private static func processRecord(for clueID: String) -> WildReportRadarProcessRecord? { switch clueID { case clueID1: return WildReportRadarProcessRecord( handlerName: "王磊", phone: "13812346721", maskedPhone: maskPhone("13812346721"), statusText: "待处理", processResult: "待现场核查,请前往空中草原观景道确认摄影师线索。", processedAt: nil, processingAt: nil ) case clueID2: return WildReportRadarProcessRecord( handlerName: "李娜", phone: "18674598210", maskedPhone: maskPhone("18674598210"), statusText: "处理中", processResult: "待现场核查,请前往塔吾萨尼步道确认摄影师线索。", processedAt: nil, processingAt: "2024-05-18 10:35" ) case clueID3: return WildReportRadarProcessRecord( handlerName: "赵强", phone: "15938267120", maskedPhone: maskPhone("15938267120"), statusText: "已处理", processResult: "已到场核查并完成劝离,处理结果已同步景区公安。", processedAt: "2024-05-17 18:26", processingAt: "2024-05-17 17:45" ) default: return nil } } private static func buildProcessingTimeline( record: WildReportRadarProcessRecord, completed: Bool ) -> [WildReportRadarProcessingTimelineItem] { let startedAt = record.processingAt ?? "2024-05-18 10:35" return [ WildReportRadarProcessingTimelineItem(title: "处理时间", time: startedAt, status: "done"), WildReportRadarProcessingTimelineItem( title: "处理完成时间", time: completed ? (record.processedAt ?? "刚刚") : "待完成", status: completed ? "done" : "current" ) ] } private static func enrichClue( _ base: WildReportRadarActiveClue, processingStarted: Bool, completed: Bool = false, completionPhoto: String? = nil ) -> WildReportRadarActiveClue { let record = processRecord(for: base.id) let isProcessing = processingStarted && !completed let timeline = (isProcessing || completed) && record != nil ? buildProcessingTimeline(record: record!, completed: completed) : [] return WildReportRadarActiveClue( id: base.id, type: base.type, displayTitle: base.displayTitle, statusText: completed ? "已处理" : (isProcessing ? "正在处理中" : base.statusText), actionText: base.actionText, nearestAbnormalText: base.nearestAbnormalText, distance: base.distance, direction: base.direction, updatedAt: completed ? (record?.processedAt ?? "刚刚") : base.updatedAt, name: base.name, storeName: base.storeName, avatar: base.avatar, summary: base.summary, detail: base.detail, distanceText: base.distanceText, reporterName: base.reporterName, reporterPhone: base.reporterPhone, maskedReporterPhone: base.maskedReporterPhone, reportContents: base.reportContents, images: base.images, processingStarted: processingStarted || completed, completed: completed, record: (isProcessing || completed) ? record : nil, processingTimeline: timeline, completionPhoto: completionPhoto ) } private static func maskPhone(_ phone: String) -> String { guard phone.count == 11, phone.allSatisfy(\.isNumber) else { return phone } return "\(phone.prefix(3))****\(phone.suffix(4))" } private static func nearestAbnormalText( blueCoordinate: CLLocationCoordinate2D, redCoordinates: [CLLocationCoordinate2D] ) -> String { guard !redCoordinates.isEmpty else { return "距离最近的异常点位0.00KM" } let minKm = redCoordinates.map { calcDistanceKm( lat1: blueCoordinate.latitude, lon1: blueCoordinate.longitude, lat2: $0.latitude, lon2: $0.longitude ) }.min() ?? 0 return String(format: "距离最近的异常点位%.2fKM", minKm) } private static func calcDistanceKm( lat1: Double, lon1: Double, lat2: Double, lon2: Double ) -> Double { let earthRadiusKm = 6371.0 let toRad = { (deg: Double) in deg * .pi / 180 } let dLat = toRad(lat2 - lat1) let dLon = toRad(lon2 - lon1) let a = sin(dLat / 2) * sin(dLat / 2) + cos(toRad(lat1)) * cos(toRad(lat2)) * sin(dLon / 2) * sin(dLon / 2) return earthRadiusKm * 2 * atan2(sqrt(a), sqrt(1 - a)) } }