Implement report supplement evidence flow
This commit is contained in:
@ -115,6 +115,129 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(response.list.first?.complaintNo, "JB20260708917")
|
||||
}
|
||||
|
||||
func testReportDetailAPILoadsRemoteDetail() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 32,
|
||||
"complaint_no": "JB20260708917",
|
||||
"title": "私下收款举报",
|
||||
"report_type": 1,
|
||||
"report_type_text": "私下收款",
|
||||
"desc": "我在观景台附近看到私下收款",
|
||||
"supplements": [],
|
||||
"report_contents": [
|
||||
{
|
||||
"type": "initial",
|
||||
"id": 0,
|
||||
"desc": "我在观景台附近看到私下收款",
|
||||
"time": "2026-07-08 16:06:59",
|
||||
"time_label": "16:06:59",
|
||||
"evidences": [
|
||||
{
|
||||
"file_type": 1,
|
||||
"file_type_text": "图片",
|
||||
"file_url": "https://example.com/report.jpg",
|
||||
"file_name": "report.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"scenic_id": 128,
|
||||
"scenic_name": "伊犁那拉提景区-5A",
|
||||
"store_user_id": 0,
|
||||
"contact_phone": "13800138000",
|
||||
"location_name": "伊犁那拉提景区-5A",
|
||||
"location_address": "江苏省扬州市邗江区瘦西湖路",
|
||||
"latitude": "32.4299428",
|
||||
"longitude": "119.4403263",
|
||||
"handle_status": 0,
|
||||
"handle_status_text": "待处理",
|
||||
"handle_remark": "",
|
||||
"evidence_summary": { "image_count": 1, "video_count": 1 },
|
||||
"evidences": [
|
||||
{
|
||||
"file_type": 1,
|
||||
"file_type_text": "图片",
|
||||
"file_url": "https://example.com/report.jpg",
|
||||
"file_name": "report.jpg"
|
||||
},
|
||||
{
|
||||
"file_type": 2,
|
||||
"file_type_text": "视频",
|
||||
"file_url": "https://example.com/report.mov",
|
||||
"file_name": "report.mov"
|
||||
}
|
||||
],
|
||||
"payment_screenshots": [
|
||||
{
|
||||
"file_type": 5,
|
||||
"file_type_text": "支付截图",
|
||||
"file_url": "https://example.com/payment.jpg",
|
||||
"file_name": "payment.jpg"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 16:06:59",
|
||||
"timeline": [
|
||||
{ "time": "2026-07-08 16:06:59", "content": "摄影师提交举报" }
|
||||
]
|
||||
},
|
||||
"time": "2026-07-08 16:31:50"
|
||||
}
|
||||
""".utf8)
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
let response = try await api.reportDetail(id: 32)
|
||||
|
||||
let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
|
||||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/detail")
|
||||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
|
||||
XCTAssertEqual(response.complaintNo, "JB20260708917")
|
||||
XCTAssertEqual(response.evidences.count, 2)
|
||||
XCTAssertEqual(response.paymentScreenshots.first?.fileName, "payment.jpg")
|
||||
XCTAssertEqual(response.timeline.first?.content, "摄影师提交举报")
|
||||
}
|
||||
|
||||
func testSupplementReportAPISendsRequestBody() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": null,
|
||||
"time": "2026-07-08 17:31:50"
|
||||
}
|
||||
""".utf8)
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
try await api.supplementReport(WildReportSupplementRequest(
|
||||
id: 32,
|
||||
desc: "补充说明",
|
||||
evidences: [
|
||||
WildReportEvidenceRequest(
|
||||
fileURL: "https://example.com/supplement.jpg",
|
||||
fileType: 1,
|
||||
fileName: "supplement.jpg"
|
||||
)
|
||||
]
|
||||
))
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/supplement")
|
||||
let bodyData = try XCTUnwrap(request.httpBody)
|
||||
let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
|
||||
XCTAssertEqual(body["id"] as? Int, 32)
|
||||
XCTAssertEqual(body["desc"] as? String, "补充说明")
|
||||
let evidences = try XCTUnwrap(body["evidences"] as? [[String: Any]])
|
||||
XCTAssertEqual(evidences.first?["file_url"] as? String, "https://example.com/supplement.jpg")
|
||||
XCTAssertEqual(evidences.first?["file_type"] as? Int, 1)
|
||||
XCTAssertEqual(evidences.first?["file_name"] as? String, "supplement.jpg")
|
||||
}
|
||||
|
||||
func testReportListItemMapsToRecord() {
|
||||
let item = makeReportListItem(
|
||||
complaintNo: "JB20260708917",
|
||||
@ -136,6 +259,31 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(record.submitTime, "2026-07-08 16:06:59")
|
||||
XCTAssertEqual(record.imageCount, 0)
|
||||
XCTAssertEqual(record.videoCount, 0)
|
||||
XCTAssertEqual(record.serverID, 32)
|
||||
}
|
||||
|
||||
func testReportDetailMapsToRecordAndSupplementData() {
|
||||
let detail = makeReportDetailData(handleStatus: 3, handleStatusText: "驳回")
|
||||
|
||||
let record = detail.toRecord()
|
||||
|
||||
XCTAssertEqual(record.serverID, 32)
|
||||
XCTAssertEqual(record.id, "JB20260708917")
|
||||
XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
|
||||
XCTAssertEqual(record.status, .rejected)
|
||||
XCTAssertEqual(record.location, "伊犁那拉提景区-5A · 江苏省扬州市邗江区瘦西湖路")
|
||||
XCTAssertEqual(record.description, "正式详情说明")
|
||||
XCTAssertEqual(record.phoneNumber, "13800138000")
|
||||
XCTAssertEqual(record.imageCount, 1)
|
||||
XCTAssertEqual(record.videoCount, 1)
|
||||
XCTAssertEqual(detail.supplementRecords.first?.summaryText, "补充说明 · 1张图片")
|
||||
}
|
||||
|
||||
func testReportStatusMapsRejected() {
|
||||
XCTAssertEqual(WildReportStatus(handleStatus: 0), .pending)
|
||||
XCTAssertEqual(WildReportStatus(handleStatus: 1), .processing)
|
||||
XCTAssertEqual(WildReportStatus(handleStatus: 2), .processed)
|
||||
XCTAssertEqual(WildReportStatus(handleStatus: 3), .rejected)
|
||||
}
|
||||
|
||||
func testReportListAllFilterOmitsHandleStatus() {
|
||||
@ -384,21 +532,165 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||||
}
|
||||
|
||||
func testSupplementEvidenceValidationAndSuccess() {
|
||||
func testSupplementEvidenceValidationAndSuccess() async throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let reportID = homeViewModel.reports[0].id
|
||||
let viewModel = WildReportSupplementEvidenceViewModel(reportID: reportID, homeViewModel: homeViewModel)
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
let uploader = MockWildReportAttachmentUploader()
|
||||
|
||||
viewModel.submit()
|
||||
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
|
||||
XCTAssertEqual(viewModel.validationMessage, "请至少填写补充说明或上传一项证据。")
|
||||
|
||||
viewModel.updateText("补充说明")
|
||||
viewModel.addImage()
|
||||
viewModel.submit()
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
|
||||
viewModel.addVideos([try makeAttachment(kind: .video, fileName: "supplement.mov")])
|
||||
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
|
||||
|
||||
XCTAssertTrue(viewModel.didSubmit)
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: reportID).first?.text, "补充说明")
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: reportID).first?.imageCount, 1)
|
||||
XCTAssertEqual(uploader.uploadedFileNames, ["supplement.jpg", "supplement.mov"])
|
||||
XCTAssertEqual(api.supplementRequests.first?.id, 32)
|
||||
XCTAssertEqual(api.supplementRequests.first?.desc, "补充说明")
|
||||
XCTAssertEqual(api.supplementRequests.first?.evidences?.map(\.fileType), [1, 2])
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.text, "补充说明")
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.imageCount, 1)
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.videoCount, 1)
|
||||
}
|
||||
|
||||
func testSupplementEvidenceFailsWithoutServerID() async {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let record = WildReportRecord(
|
||||
serverID: nil,
|
||||
id: "JBLOCAL",
|
||||
title: "摄影师举报",
|
||||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||||
status: .pending,
|
||||
location: "测试景区",
|
||||
submitTime: "2026-07-08 16:06:59",
|
||||
description: "本地记录",
|
||||
wechatID: "",
|
||||
phoneNumber: "",
|
||||
imageCount: 1,
|
||||
videoCount: 0,
|
||||
handlerInfo: nil
|
||||
)
|
||||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||||
viewModel.updateText("补充说明")
|
||||
|
||||
await viewModel.submit(
|
||||
api: MockWildPhotographerReportAPI(),
|
||||
uploader: MockWildReportAttachmentUploader(),
|
||||
scenicId: 128
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.validationMessage, "当前举报暂未同步到服务器,无法补充证据")
|
||||
XCTAssertFalse(viewModel.didSubmit)
|
||||
}
|
||||
|
||||
func testSupplementEvidenceTextAndAttachmentLimits() throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||||
|
||||
viewModel.updateText(String(repeating: "测", count: 520))
|
||||
XCTAssertEqual(viewModel.text.count, 500)
|
||||
|
||||
let images = try (0..<10).map { try makeAttachment(kind: .image, fileName: "image\($0).jpg") }
|
||||
viewModel.addImages(images)
|
||||
XCTAssertEqual(viewModel.images.count, 9)
|
||||
|
||||
viewModel.removeAttachment(viewModel.images[0])
|
||||
let videos = try (0..<4).map { try makeAttachment(kind: .video, fileName: "video\($0).mov") }
|
||||
viewModel.addVideos(videos)
|
||||
XCTAssertEqual(viewModel.videos.count, 1)
|
||||
}
|
||||
|
||||
func testSupplementEvidenceUploadFailureDoesNotSubmit() async throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
let uploader = MockWildReportAttachmentUploader()
|
||||
uploader.error = URLError(.cannotWriteToFile)
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
|
||||
|
||||
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
|
||||
|
||||
XCTAssertFalse(viewModel.didSubmit)
|
||||
XCTAssertTrue(api.supplementRequests.isEmpty)
|
||||
XCTAssertEqual(viewModel.validationMessage, URLError(.cannotWriteToFile).localizedDescription)
|
||||
}
|
||||
|
||||
func testSupplementEvidenceAPIErrorDoesNotSucceed() async throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.supplementError = URLError(.badServerResponse)
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
|
||||
|
||||
await viewModel.submit(
|
||||
api: api,
|
||||
uploader: MockWildReportAttachmentUploader(),
|
||||
scenicId: 128
|
||||
)
|
||||
|
||||
XCTAssertFalse(viewModel.didSubmit)
|
||||
XCTAssertEqual(api.supplementRequests.count, 0)
|
||||
XCTAssertEqual(viewModel.validationMessage, URLError(.badServerResponse).localizedDescription)
|
||||
}
|
||||
|
||||
func testDetailViewModelLoadsRemoteDetailWhenServerIDExists() async {
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.reportDetailResponses = [makeReportDetailData()]
|
||||
|
||||
await viewModel.loadDetailIfNeeded(api: api)
|
||||
|
||||
XCTAssertEqual(api.reportDetailRequests, [32])
|
||||
XCTAssertEqual(viewModel.record.description, "正式详情说明")
|
||||
XCTAssertEqual(viewModel.reporterMediaItems.count, 2)
|
||||
XCTAssertEqual(viewModel.paymentScreenshotItems.count, 1)
|
||||
XCTAssertEqual(viewModel.progressSteps.first?.title, "摄影师提交举报")
|
||||
}
|
||||
|
||||
func testDetailViewModelFailureKeepsInitialRecord() async {
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.reportDetailError = URLError(.badServerResponse)
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
await viewModel.loadDetailIfNeeded(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.record.description, "测试说明")
|
||||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||||
}
|
||||
|
||||
func testDetailViewModelWithoutServerIDSkipsRequest() async {
|
||||
let record = WildReportRecord(
|
||||
id: "JBLOCAL",
|
||||
title: "摄影师举报",
|
||||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||||
status: .pending,
|
||||
location: "测试景区",
|
||||
submitTime: "2026-07-08 16:06:59",
|
||||
description: "本地提交记录",
|
||||
wechatID: "",
|
||||
phoneNumber: "",
|
||||
imageCount: 1,
|
||||
videoCount: 0,
|
||||
handlerInfo: nil
|
||||
)
|
||||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
|
||||
await viewModel.loadDetailIfNeeded(api: api)
|
||||
|
||||
XCTAssertTrue(api.reportDetailRequests.isEmpty)
|
||||
XCTAssertEqual(viewModel.record.description, "本地提交记录")
|
||||
}
|
||||
}
|
||||
|
||||
@ -410,6 +702,11 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
var reportListResponses: [WildReportListResponse] = []
|
||||
var reportListRequests: [WildReportListRequest] = []
|
||||
var reportListError: Error?
|
||||
var reportDetailResponses: [WildReportDetailData] = []
|
||||
var reportDetailRequests: [Int] = []
|
||||
var reportDetailError: Error?
|
||||
var supplementRequests: [WildReportSupplementRequest] = []
|
||||
var supplementError: Error?
|
||||
|
||||
func reportTypes() async throws -> WildReportTypeListResponse {
|
||||
if let error { throw error }
|
||||
@ -421,6 +718,11 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
submitRequests.append(request)
|
||||
}
|
||||
|
||||
func supplementReport(_ request: WildReportSupplementRequest) async throws {
|
||||
if let supplementError { throw supplementError }
|
||||
supplementRequests.append(request)
|
||||
}
|
||||
|
||||
func reportList(_ request: WildReportListRequest) async throws -> WildReportListResponse {
|
||||
if let reportListError { throw reportListError }
|
||||
reportListRequests.append(request)
|
||||
@ -429,6 +731,13 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
}
|
||||
return reportListResponses.removeFirst()
|
||||
}
|
||||
|
||||
func reportDetail(id: Int) async throws -> WildReportDetailData {
|
||||
if let reportDetailError { throw reportDetailError }
|
||||
reportDetailRequests.append(id)
|
||||
guard !reportDetailResponses.isEmpty else { return WildReportDetailData(id: id) }
|
||||
return reportDetailResponses.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -462,6 +771,7 @@ private func makeAttachment(kind: WildReportAttachmentKind, fileName: String) th
|
||||
}
|
||||
|
||||
private func makeReportListItem(
|
||||
id: Int = 32,
|
||||
complaintNo: String,
|
||||
title: String = "疑似打野摄影师举报",
|
||||
reportType: Int = 2,
|
||||
@ -470,7 +780,7 @@ private func makeReportListItem(
|
||||
handleStatusText: String = "待处理"
|
||||
) -> WildReportListItem {
|
||||
WildReportListItem(
|
||||
id: Int(complaintNo.filter(\.isNumber)) ?? 1,
|
||||
id: id,
|
||||
complaintNo: complaintNo,
|
||||
title: title,
|
||||
reportType: reportType,
|
||||
@ -486,3 +796,73 @@ private func makeReportListItem(
|
||||
createdAt: "2026-07-08 16:06:59"
|
||||
)
|
||||
}
|
||||
|
||||
private func makeReportDetailData(
|
||||
handleStatus: Int = 0,
|
||||
handleStatusText: String = "待处理"
|
||||
) -> WildReportDetailData {
|
||||
WildReportDetailData(
|
||||
id: 32,
|
||||
complaintNo: "JB20260708917",
|
||||
title: "私下收款举报",
|
||||
reportType: 1,
|
||||
reportTypeText: "私下收款",
|
||||
desc: "正式详情说明",
|
||||
supplements: [
|
||||
WildReportDetailContent(
|
||||
type: "supplement",
|
||||
id: 7,
|
||||
desc: "补充说明",
|
||||
time: "2026-07-08 17:06:59",
|
||||
timeLabel: "17:06:59",
|
||||
evidences: [
|
||||
WildReportDetailEvidence(
|
||||
fileType: 1,
|
||||
fileTypeText: "图片",
|
||||
fileURL: "https://example.com/supplement.jpg",
|
||||
fileName: "supplement.jpg"
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
reportContents: [],
|
||||
scenicId: 128,
|
||||
scenicName: "伊犁那拉提景区-5A",
|
||||
storeUserId: 0,
|
||||
contactPhone: "13800138000",
|
||||
locationName: "伊犁那拉提景区-5A",
|
||||
locationAddress: "江苏省扬州市邗江区瘦西湖路",
|
||||
latitude: "32.4299428",
|
||||
longitude: "119.4403263",
|
||||
handleStatus: handleStatus,
|
||||
handleStatusText: handleStatusText,
|
||||
handleRemark: "处理备注",
|
||||
evidenceSummary: WildReportEvidenceSummary(imageCount: 1, videoCount: 1),
|
||||
evidences: [
|
||||
WildReportDetailEvidence(
|
||||
fileType: 1,
|
||||
fileTypeText: "图片",
|
||||
fileURL: "https://example.com/report.jpg",
|
||||
fileName: "report.jpg"
|
||||
),
|
||||
WildReportDetailEvidence(
|
||||
fileType: 2,
|
||||
fileTypeText: "视频",
|
||||
fileURL: "https://example.com/report.mov",
|
||||
fileName: "report.mov"
|
||||
)
|
||||
],
|
||||
paymentScreenshots: [
|
||||
WildReportDetailEvidence(
|
||||
fileType: 5,
|
||||
fileTypeText: "支付截图",
|
||||
fileURL: "https://example.com/payment.jpg",
|
||||
fileName: "payment.jpg"
|
||||
)
|
||||
],
|
||||
timeline: [
|
||||
WildReportDetailTimelineItem(time: "2026-07-08 16:06:59", content: "摄影师提交举报")
|
||||
],
|
||||
createdAt: "2026-07-08 16:06:59"
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user