Files
suixinkan_uikit/suixinkan/Features/WildPhotographerReport/Services/WildPhotographerReportMockStore.swift

605 lines
24 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.

//
// 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: .suspectedWild,
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: .suspectedWild,
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: .noWorkBadge,
status: .pending,
location: "那拉提景区 · 盘龙古道停车区",
submitTime: "2026-06-16 09:15",
description: "停车区附近有摄影师未佩戴工牌开展拍摄服务,已收到图片和位置线索。",
wechatID: "",
phoneNumber: "13987654321",
imageCount: 1,
videoCount: 0,
handlerInfo: nil
),
WildReportRecord(
id: "JB20260615002",
title: "摄影师举报",
reportType: .other,
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<String> = [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))
}
}