feat: connect wild report risk map
This commit is contained in:
@ -3,6 +3,7 @@
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@ -238,6 +239,121 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(evidences.first?["file_name"] as? String, "supplement.jpg")
|
||||
}
|
||||
|
||||
func testRiskMapAPISendsRequestBodyAndDecodesMarkers() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"scenic_id": 128,
|
||||
"scenic_name": "伊犁那拉提景区-5A",
|
||||
"legend": [
|
||||
{ "marker_type": "red", "title": "疑似打野" }
|
||||
],
|
||||
"green_marker_tip": "绿色为景区内摄影师",
|
||||
"markers": [
|
||||
{
|
||||
"marker_type": "red",
|
||||
"id": 32,
|
||||
"latitude": "43.4068",
|
||||
"longitude": "84.0582",
|
||||
"location_name": "空中草原观景道",
|
||||
"title": "线索 ID:32",
|
||||
"report_type_text": "疑似打野",
|
||||
"handle_status": 0,
|
||||
"distance_m": 320,
|
||||
"created_at": "2026-07-08 16:06:59"
|
||||
},
|
||||
{
|
||||
"marker_type": "yellow",
|
||||
"id": 33,
|
||||
"latitude": "43.3652",
|
||||
"longitude": "84.0385",
|
||||
"location_name": "塔吾萨尼步道",
|
||||
"title": "线索 ID:33",
|
||||
"report_type_text": "处理中",
|
||||
"handle_status": "1",
|
||||
"distance_m": 1280,
|
||||
"created_at": "2026-07-08 16:08:59"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": "2026-07-08 18:31:50"
|
||||
}
|
||||
""".utf8)
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
let response = try await api.riskMap(WildReportRiskMapRequest(
|
||||
scenicId: 128,
|
||||
latitude: 43.3779,
|
||||
longitude: 84.0217
|
||||
))
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/risk-map")
|
||||
let bodyData = try XCTUnwrap(request.httpBody)
|
||||
let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
|
||||
XCTAssertEqual(body["scenic_id"] as? Int, 128)
|
||||
XCTAssertEqual(body["latitude"] as? Double, 43.3779)
|
||||
XCTAssertEqual(body["longitude"] as? Double, 84.0217)
|
||||
XCTAssertEqual(response.scenicName, "伊犁那拉提景区-5A")
|
||||
XCTAssertEqual(response.greenMarkerTip, "绿色为景区内摄影师")
|
||||
XCTAssertEqual(response.markers.map(\.markerType), ["red", "yellow"])
|
||||
XCTAssertEqual(response.markers.last?.handleStatus?.displayText, WildReportStatus.processing.rawValue)
|
||||
}
|
||||
|
||||
func testMarkerDetailAPISendsRequestBodyAndDecodesGreenDetail() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"marker_type": "green",
|
||||
"id": 901,
|
||||
"title": "景区内摄影师",
|
||||
"tag": "认证",
|
||||
"name": "阿依达娜",
|
||||
"avatar": "https://example.com/avatar.jpg",
|
||||
"store_name": "空中草原旅拍店",
|
||||
"online": true,
|
||||
"location_name": "空中草原",
|
||||
"latitude": "43.4042",
|
||||
"longitude": "84.0186",
|
||||
"distance_m": 430,
|
||||
"green_marker_tip": "绿色为景区内摄影师",
|
||||
"activity_text": "当前在空中草原区域服务游客拍摄。"
|
||||
},
|
||||
"time": "2026-07-08 18:31:50"
|
||||
}
|
||||
""".utf8)
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
let response = try await api.markerDetail(WildReportMarkerDetailRequest(
|
||||
scenicId: 128,
|
||||
markerType: "green",
|
||||
id: 901,
|
||||
latitude: 43.3779,
|
||||
longitude: 84.0217
|
||||
))
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/marker-detail")
|
||||
let bodyData = try XCTUnwrap(request.httpBody)
|
||||
let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
|
||||
XCTAssertEqual(body["scenic_id"] as? Int, 128)
|
||||
XCTAssertEqual(body["marker_type"] as? String, "green")
|
||||
XCTAssertEqual(body["id"] as? Int, 901)
|
||||
XCTAssertEqual(body["latitude"] as? Double, 43.3779)
|
||||
XCTAssertEqual(body["longitude"] as? Double, 84.0217)
|
||||
XCTAssertEqual(response.name, "阿依达娜")
|
||||
XCTAssertEqual(response.storeName, "空中草原旅拍店")
|
||||
XCTAssertTrue(response.online)
|
||||
}
|
||||
|
||||
func testReportListItemMapsToRecord() {
|
||||
let item = makeReportListItem(
|
||||
complaintNo: "JB20260708917",
|
||||
@ -640,6 +756,139 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.validationMessage, URLError(.badServerResponse).localizedDescription)
|
||||
}
|
||||
|
||||
func testRiskMapViewModelRejectsInvalidScenicID() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
await viewModel.loadInitial(
|
||||
api: api,
|
||||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||||
scenicId: 0
|
||||
)
|
||||
|
||||
XCTAssertTrue(api.riskMapRequests.isEmpty)
|
||||
XCTAssertEqual(messages.last, "请先选择景区后再查看风险地图")
|
||||
}
|
||||
|
||||
func testRiskMapViewModelLoadsWithLocationAndMapsMarkerTypes() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
|
||||
await viewModel.loadInitial(
|
||||
api: api,
|
||||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||||
scenicId: 128,
|
||||
scenicName: "测试景区"
|
||||
)
|
||||
|
||||
XCTAssertEqual(api.riskMapRequests.first?.scenicId, 128)
|
||||
XCTAssertEqual(api.riskMapRequests.first?.latitude, 43.3779)
|
||||
XCTAssertEqual(api.riskMapRequests.first?.longitude, 84.0217)
|
||||
XCTAssertEqual(viewModel.scenicAreaName, "伊犁那拉提景区-5A")
|
||||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .selfLocation })
|
||||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .wildPhotographer })
|
||||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .processingClue })
|
||||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .peerPhotographer })
|
||||
XCTAssertEqual(viewModel.selectedMarker?.kind, .selfLocation)
|
||||
let yellow = viewModel.markers.first { $0.kind == .processingClue }
|
||||
XCTAssertEqual(yellow.flatMap { marker in
|
||||
if case .activeClue(let clue) = marker.detail { return clue.distanceText }
|
||||
return nil
|
||||
}, "1.3公里")
|
||||
}
|
||||
|
||||
func testRiskMapViewModelLoadsWithoutLocationWhenLocationFails() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
|
||||
await viewModel.loadInitial(
|
||||
api: api,
|
||||
locationProvider: MockRiskMapLocationProvider(.failure),
|
||||
scenicId: 128
|
||||
)
|
||||
|
||||
XCTAssertEqual(api.riskMapRequests.first?.scenicId, 128)
|
||||
XCTAssertNil(api.riskMapRequests.first?.latitude)
|
||||
XCTAssertNil(api.riskMapRequests.first?.longitude)
|
||||
XCTAssertFalse(viewModel.markers.contains { $0.kind == .selfLocation })
|
||||
}
|
||||
|
||||
func testRiskMapViewModelLoadsMarkerDetailAndUpdatesSelectedClue() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
api.markerDetailResponses = [
|
||||
WildReportMarkerDetailResponse(
|
||||
markerType: "red",
|
||||
id: 32,
|
||||
title: "线索 ID:32",
|
||||
locationName: "空中草原观景道",
|
||||
latitude: "43.4068",
|
||||
longitude: "84.0582",
|
||||
distanceM: 320,
|
||||
reportTypeText: "疑似打野",
|
||||
handleStatus: WildReportDecodedHandleStatus(intValue: 0),
|
||||
desc: "多人结伴揽客",
|
||||
reporterName: "张三",
|
||||
reporterPhone: "13333333333",
|
||||
createdAt: "2026-07-08 16:06:59"
|
||||
)
|
||||
]
|
||||
await viewModel.loadInitial(
|
||||
api: api,
|
||||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||||
scenicId: 128
|
||||
)
|
||||
let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
|
||||
|
||||
await viewModel.selectMarker(redMarker, api: api, scenicId: 128)
|
||||
|
||||
XCTAssertEqual(api.markerDetailRequests.first?.markerType, "red")
|
||||
XCTAssertEqual(api.markerDetailRequests.first?.id, 32)
|
||||
XCTAssertEqual(viewModel.selectedClue?.reporterName, "张三")
|
||||
XCTAssertEqual(viewModel.selectedClue?.maskedReporterPhone, "133****3333")
|
||||
XCTAssertEqual(viewModel.selectedClue?.detail, "多人结伴揽客")
|
||||
}
|
||||
|
||||
func testRiskMapViewModelMarkerDetailFailureKeepsSummary() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
api.markerDetailError = URLError(.badServerResponse)
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
await viewModel.loadInitial(
|
||||
api: api,
|
||||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||||
scenicId: 128
|
||||
)
|
||||
let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
|
||||
|
||||
await viewModel.selectMarker(redMarker, api: api, scenicId: 128)
|
||||
|
||||
XCTAssertEqual(viewModel.selectedClue?.summary, "疑似打野")
|
||||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||||
}
|
||||
|
||||
func testDetailViewModelLoadsRemoteDetailWhenServerIDExists() async {
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||||
@ -707,6 +956,12 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
var reportDetailError: Error?
|
||||
var supplementRequests: [WildReportSupplementRequest] = []
|
||||
var supplementError: Error?
|
||||
var riskMapResponses: [WildReportRiskMapResponse] = []
|
||||
var riskMapRequests: [WildReportRiskMapRequest] = []
|
||||
var riskMapError: Error?
|
||||
var markerDetailResponses: [WildReportMarkerDetailResponse] = []
|
||||
var markerDetailRequests: [WildReportMarkerDetailRequest] = []
|
||||
var markerDetailError: Error?
|
||||
|
||||
func reportTypes() async throws -> WildReportTypeListResponse {
|
||||
if let error { throw error }
|
||||
@ -723,6 +978,24 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
supplementRequests.append(request)
|
||||
}
|
||||
|
||||
func riskMap(_ request: WildReportRiskMapRequest) async throws -> WildReportRiskMapResponse {
|
||||
if let riskMapError { throw riskMapError }
|
||||
riskMapRequests.append(request)
|
||||
guard !riskMapResponses.isEmpty else {
|
||||
return WildReportRiskMapResponse(scenicId: request.scenicId, scenicName: "测试景区")
|
||||
}
|
||||
return riskMapResponses.removeFirst()
|
||||
}
|
||||
|
||||
func markerDetail(_ request: WildReportMarkerDetailRequest) async throws -> WildReportMarkerDetailResponse {
|
||||
if let markerDetailError { throw markerDetailError }
|
||||
markerDetailRequests.append(request)
|
||||
guard !markerDetailResponses.isEmpty else {
|
||||
return WildReportMarkerDetailResponse(markerType: request.markerType, id: request.id)
|
||||
}
|
||||
return markerDetailResponses.removeFirst()
|
||||
}
|
||||
|
||||
func reportList(_ request: WildReportListRequest) async throws -> WildReportListResponse {
|
||||
if let reportListError { throw reportListError }
|
||||
reportListRequests.append(request)
|
||||
@ -740,6 +1013,41 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
}
|
||||
}
|
||||
|
||||
private enum MockRiskMapLocationResult: Sendable {
|
||||
case success(latitude: Double, longitude: Double)
|
||||
case failure
|
||||
}
|
||||
|
||||
private struct MockRiskMapLocationProvider: LocationProviding {
|
||||
private let result: MockRiskMapLocationResult
|
||||
|
||||
init(_ result: MockRiskMapLocationResult) {
|
||||
self.result = result
|
||||
}
|
||||
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
switch result {
|
||||
case .success(let latitude, let longitude):
|
||||
return HomeLocationSnapshot(latitude: latitude, longitude: longitude, address: "测试地址")
|
||||
case .failure:
|
||||
throw LocationProviderError.locationFailed
|
||||
}
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
switch result {
|
||||
case .success(let latitude, let longitude):
|
||||
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
case .failure:
|
||||
throw LocationProviderError.locationFailed
|
||||
}
|
||||
}
|
||||
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
"测试地址"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockWildReportAttachmentUploader: WildReportAttachmentUploading {
|
||||
var uploadedFileNames: [String] = []
|
||||
@ -770,6 +1078,55 @@ private func makeAttachment(kind: WildReportAttachmentKind, fileName: String) th
|
||||
)
|
||||
}
|
||||
|
||||
private func makeRiskMapResponse() -> WildReportRiskMapResponse {
|
||||
WildReportRiskMapResponse(
|
||||
scenicId: 128,
|
||||
scenicName: "伊犁那拉提景区-5A",
|
||||
legend: [WildReportRiskLegendItem(markerType: "red", title: "疑似打野")],
|
||||
greenMarkerTip: "绿色为景区内摄影师",
|
||||
markers: [
|
||||
WildReportRiskMapMarkerDTO(
|
||||
markerType: "red",
|
||||
id: 32,
|
||||
latitude: "43.4068",
|
||||
longitude: "84.0582",
|
||||
locationName: "空中草原观景道",
|
||||
title: "线索 ID:32",
|
||||
reportTypeText: "疑似打野",
|
||||
handleStatus: WildReportDecodedHandleStatus(intValue: 0),
|
||||
distanceM: 320,
|
||||
createdAt: "2026-07-08 16:06:59"
|
||||
),
|
||||
WildReportRiskMapMarkerDTO(
|
||||
markerType: "yellow",
|
||||
id: 33,
|
||||
latitude: "43.3652",
|
||||
longitude: "84.0385",
|
||||
locationName: "塔吾萨尼步道",
|
||||
title: "线索 ID:33",
|
||||
reportTypeText: "处理中",
|
||||
handleStatus: WildReportDecodedHandleStatus(intValue: 1),
|
||||
distanceM: 1280,
|
||||
createdAt: "2026-07-08 16:08:59"
|
||||
),
|
||||
WildReportRiskMapMarkerDTO(
|
||||
markerType: "green",
|
||||
id: 901,
|
||||
latitude: "43.4042",
|
||||
longitude: "84.0186",
|
||||
locationName: "空中草原",
|
||||
title: "景区内摄影师",
|
||||
distanceM: 430,
|
||||
createdAt: "2026-07-08 16:09:59",
|
||||
name: "阿依达娜",
|
||||
storeName: "空中草原旅拍店",
|
||||
online: true,
|
||||
activityText: "当前在空中草原区域服务游客拍摄。"
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
private func makeReportListItem(
|
||||
id: Int = 32,
|
||||
complaintNo: String,
|
||||
|
||||
Reference in New Issue
Block a user