feat: update app workflows and permissions

This commit is contained in:
2026-07-09 17:34:00 +08:00
parent 43e6133c21
commit 8e356973bd
44 changed files with 2944 additions and 307 deletions

View File

@ -233,6 +233,17 @@ final class WildPhotographerReportTests: XCTestCase {
"handle_status": 0,
"handle_status_text": "待处理",
"handle_remark": "",
"handled_at": "2026-07-09 14:18:40",
"handler": {
"id": 43,
"name": "管铜彬",
"phone": "132****9413",
"started_at": "2026-07-09 14:18:15",
"finished_at": "2026-07-09 14:18:40",
"remark": "我是处理意见"
},
"handler_name": "管铜彬",
"handler_phone": "132****9413",
"evidence_summary": { "image_count": 1, "video_count": 1 },
"evidences": [
{
@ -248,6 +259,24 @@ final class WildPhotographerReportTests: XCTestCase {
"file_name": "report.mov"
}
],
"handle_evidences": [
{
"evidence_kind": 2,
"evidence_kind_text": "处理凭证",
"file_type": 1,
"file_type_text": "图片",
"file_url": "http://tmp/handler-one.jpg",
"file_name": "handler-one.jpg"
},
{
"evidence_kind": 2,
"evidence_kind_text": "处理凭证",
"file_type": 1,
"file_type_text": "图片",
"file_url": "http://tmp/handler-two.png",
"file_name": "handler-two.png"
}
],
"payment_screenshots": [
{
"file_type": 5,
@ -276,6 +305,55 @@ final class WildPhotographerReportTests: XCTestCase {
XCTAssertEqual(response.evidences.count, 2)
XCTAssertEqual(response.paymentScreenshots.first?.fileName, "payment.jpg")
XCTAssertEqual(response.timeline.first?.content, "")
XCTAssertEqual(response.handler?.name, "")
XCTAssertEqual(response.handler?.phone, "132****9413")
XCTAssertEqual(response.handler?.startedAt, "2026-07-09 14:18:15")
XCTAssertEqual(response.handler?.finishedAt, "2026-07-09 14:18:40")
XCTAssertEqual(response.handler?.remark, "")
XCTAssertEqual(response.handledAt, "2026-07-09 14:18:40")
XCTAssertEqual(response.handleEvidences.count, 2)
XCTAssertEqual(response.handleEvidences.first?.evidenceKind, 2)
XCTAssertEqual(response.handleEvidences.first?.evidenceKindText, "")
XCTAssertEqual(response.handleEvidences.first?.fileURL, "http://tmp/handler-one.jpg")
XCTAssertEqual(response.handleEvidences.last?.fileName, "handler-two.png")
}
func testReportShareAPILoadsMiniProgramConfig() async throws {
let json = Data("""
{
"code": 100000,
"msg": "success",
"data": {
"userName": "wx8c85189bf3bdda29",
"path": "pages/scenic/radar/report-detail/index?id=32",
"title": "",
"imagePath": "/assets/share/report-share-cover.png",
"webpageUrl": "https://www.youfuntour.com",
"withShareTicket": true,
"miniprogramType": 0,
"scene": 0
},
"time": "2026-07-09 15:42:19"
}
""".utf8)
let session = MockURLSession(responses: [json])
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
let response = try await api.reportShare(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/share")
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
XCTAssertEqual(response.userName, "wx8c85189bf3bdda29")
XCTAssertEqual(response.path, "pages/scenic/radar/report-detail/index?id=32")
XCTAssertEqual(response.title, "")
XCTAssertEqual(response.imagePath, "/assets/share/report-share-cover.png")
XCTAssertEqual(response.webpageURL, "https://www.youfuntour.com")
XCTAssertTrue(response.withShareTicket)
XCTAssertEqual(response.miniProgramType, .release)
XCTAssertEqual(response.scene, .session)
XCTAssertEqual(response.payload.userName, "wx8c85189bf3bdda29")
XCTAssertEqual(response.payload.path, "pages/scenic/radar/report-detail/index?id=32")
}
func testSupplementReportAPISendsRequestBody() async throws {
@ -472,6 +550,106 @@ final class WildPhotographerReportTests: XCTestCase {
XCTAssertEqual(detail.supplementRecords.first?.summaryText, " · 1")
}
func testSupplementRecordSequenceTitleStartsFromDisplayOrder() {
let titles = (0..<3).map {
WildReportSupplementDisplayFormatter.sequenceTitle(forDisplayIndex: $0)
}
XCTAssertEqual(titles, ["1", "2", "3"])
}
func testReportDetailMapsHandlerInfoFromHandlerFields() throws {
let detail = makeReportDetailData(
handleStatus: 2,
handleStatusText: "",
handleRemark: "",
handledAt: "2026-07-09 14:18:40",
handler: WildReportDetailHandler(
id: 43,
name: "",
phone: "132****9413",
startedAt: "2026-07-09 14:18:15",
finishedAt: "2026-07-09 14:18:40",
remark: ""
),
handleEvidences: [
WildReportDetailEvidence(
fileType: 1,
fileTypeText: "",
fileURL: "http://tmp/handler-one.jpg",
fileName: "handler-one.jpg",
evidenceKind: 2,
evidenceKindText: "处理凭证"
),
WildReportDetailEvidence(
fileType: 1,
fileTypeText: "图片",
fileURL: "http://tmp/handler-two.png",
fileName: "handler-two.png",
evidenceKind: 2,
evidenceKindText: "处理凭证"
)
]
)
let info = try XCTUnwrap(detail.toRecord().handlerInfo)
XCTAssertEqual(info.handlerName, "管铜彬")
XCTAssertEqual(info.handlerPhone, "132****9413")
XCTAssertEqual(info.processingAt, "2026-07-09 14:18:15")
XCTAssertEqual(info.processedAt, "2026-07-09 14:18:40")
XCTAssertEqual(info.processOpinion, "我是处理意见")
XCTAssertEqual(info.completionMedia.count, 2)
XCTAssertEqual(info.completionMedia.first?.coverURL, "http://tmp/handler-one.jpg")
XCTAssertEqual(info.completionMediaSummary, "2张")
}
func testReportDetailMapsHandlerInfoFromFallbackFields() throws {
let detail = makeReportDetailData(
handleStatus: 2,
handleStatusText: "已处理",
handleRemark: "fallback 处理意见",
handledAt: "2026-07-09 14:18:40",
handlerName: "管铜彬",
handlerPhone: "132****9413"
)
let info = try XCTUnwrap(detail.toRecord().handlerInfo)
XCTAssertEqual(info.handlerName, "管铜彬")
XCTAssertEqual(info.handlerPhone, "132****9413")
XCTAssertEqual(info.processingAt, "")
XCTAssertEqual(info.processedAt, "2026-07-09 14:18:40")
XCTAssertEqual(info.processOpinion, "fallback 处理意见")
}
func testReportShareDataDecodesSnakeCaseAndMapsPayload() throws {
let json = Data("""
{
"user_name": "gh_report_original_id",
"path": "pages/report/detail?id=32",
"title": "",
"description": "",
"image_path": "https://example.com/share.png",
"webpage_url": "https://www.youfuntour.com/report/32",
"with_share_ticket": true,
"mini_program_type": 2,
"scene": 0,
"disable_forward": true
}
""".utf8)
let data = try JSONDecoder().decode(WildReportMiniProgramShareData.self, from: json)
XCTAssertEqual(data.userName, "gh_report_original_id")
XCTAssertEqual(data.webpageURL, "https://www.youfuntour.com/report/32")
XCTAssertEqual(data.miniProgramType, .preview)
XCTAssertEqual(data.scene, .session)
XCTAssertEqual(data.payload.description, "查看举报处理进度")
XCTAssertTrue(data.payload.withShareTicket)
XCTAssertTrue(data.payload.disableForward)
}
func testReportStatusMapsRejected() {
XCTAssertEqual(WildReportStatus(handleStatus: 0), .pending)
XCTAssertEqual(WildReportStatus(handleStatus: 1), .processing)
@ -731,6 +909,61 @@ final class WildPhotographerReportTests: XCTestCase {
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
}
func testListViewModelLoadsMiniProgramSharePayload() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let api = MockWildPhotographerReportAPI()
api.reportShareResponse = WildReportMiniProgramShareData(
userName: "gh_report_original_id",
path: "pages/report/detail?id=32",
title: "私下收款举报",
description: "查看举报处理进度",
webpageURL: "https://www.youfuntour.com/report/32",
withShareTicket: true,
miniProgramTypeValue: WeChatMiniProgramType.preview.rawValue
)
let maybePayload = await viewModel.shareReport(record, api: api)
let payload = try XCTUnwrap(maybePayload)
XCTAssertEqual(api.reportShareRequests, ["32"])
XCTAssertEqual(payload.title, "私下收款举报")
XCTAssertEqual(payload.userName, "gh_report_original_id")
XCTAssertEqual(payload.path, "pages/report/detail?id=32")
XCTAssertEqual(payload.miniProgramType, .preview)
XCTAssertTrue(payload.withShareTicket)
}
func testListViewModelShareRejectsLocalRecord() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
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 api = MockWildPhotographerReportAPI()
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
let payload = await viewModel.shareReport(record, api: api)
XCTAssertNil(payload)
XCTAssertTrue(api.reportShareRequests.isEmpty)
XCTAssertEqual(messages.last, "当前举报暂未同步到服务器,无法分享")
}
func testSupplementEvidenceValidationAndSuccess() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
@ -1023,7 +1256,28 @@ final class WildPhotographerReportTests: XCTestCase {
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildPhotographerReportDetailViewModel(record: record)
let api = MockWildPhotographerReportAPI()
api.reportDetailResponses = [makeReportDetailData()]
api.reportDetailResponses = [
makeReportDetailData(
handleStatus: 2,
handleStatusText: "已处理",
handler: WildReportDetailHandler(
id: 43,
name: "管铜彬",
phone: "132****9413",
startedAt: "2026-07-09 14:18:15",
finishedAt: "2026-07-09 14:18:40",
remark: "我是处理意见"
),
handleEvidences: [
WildReportDetailEvidence(
fileType: 1,
fileTypeText: "图片",
fileURL: "http://tmp/handler-one.jpg",
fileName: "handler-one.jpg"
)
]
)
]
await viewModel.loadDetailIfNeeded(api: api)
@ -1032,6 +1286,10 @@ final class WildPhotographerReportTests: XCTestCase {
XCTAssertEqual(viewModel.reporterMediaItems.count, 2)
XCTAssertEqual(viewModel.paymentScreenshotItems.count, 1)
XCTAssertEqual(viewModel.progressSteps.first?.title, "摄影师提交举报")
XCTAssertTrue(viewModel.showHandlerInfo)
XCTAssertEqual(viewModel.handlerInfo?.handlerName, "管铜彬")
XCTAssertEqual(viewModel.handlerInfo?.processingAt, "2026-07-09 14:18:15")
XCTAssertEqual(viewModel.handlerInfo?.completionMedia.first?.coverURL, "http://tmp/handler-one.jpg")
}
func testDetailViewModelFailureKeepsInitialRecord() async {
@ -1048,6 +1306,27 @@ final class WildPhotographerReportTests: XCTestCase {
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
}
func testDetailViewModelLoadsMiniProgramSharePayload() async throws {
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildPhotographerReportDetailViewModel(record: record)
let api = MockWildPhotographerReportAPI()
api.reportShareResponse = WildReportMiniProgramShareData(
userName: "gh_report_original_id",
path: "pages/report/detail?id=32",
title: "私下收款举报",
webpageURL: "https://www.youfuntour.com/report/32",
miniProgramTypeValue: WeChatMiniProgramType.test.rawValue
)
let maybePayload = await viewModel.shareReport(api: api)
let payload = try XCTUnwrap(maybePayload)
XCTAssertEqual(api.reportShareRequests, ["32"])
XCTAssertEqual(payload.webpageURL, "https://www.youfuntour.com/report/32")
XCTAssertEqual(payload.userName, "gh_report_original_id")
XCTAssertEqual(payload.miniProgramType, .test)
}
func testDetailViewModelWithoutServerIDSkipsRequest() async {
let record = WildReportRecord(
id: "JBLOCAL",
@ -1087,6 +1366,9 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
var reportDetailResponses: [WildReportDetailData] = []
var reportDetailRequests: [Int] = []
var reportDetailError: Error?
var reportShareResponse = WildReportMiniProgramShareData()
var reportShareRequests: [String] = []
var reportShareError: Error?
var supplementRequests: [WildReportSupplementRequest] = []
var supplementError: Error?
var riskMapResponses: [WildReportRiskMapResponse] = []
@ -1150,6 +1432,12 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
guard !reportDetailResponses.isEmpty else { return WildReportDetailData(id: id) }
return reportDetailResponses.removeFirst()
}
func reportShare(id: String) async throws -> WildReportMiniProgramShareData {
if let reportShareError { throw reportShareError }
reportShareRequests.append(id)
return reportShareResponse
}
}
private enum MockRiskMapLocationResult: Sendable {
@ -1312,7 +1600,13 @@ private func makeReportListItem(
private func makeReportDetailData(
handleStatus: Int = 0,
handleStatusText: String = "待处理"
handleStatusText: String = "待处理",
handleRemark: String = "处理备注",
handledAt: String = "",
handler: WildReportDetailHandler? = nil,
handlerName: String = "",
handlerPhone: String = "",
handleEvidences: [WildReportDetailEvidence] = []
) -> WildReportDetailData {
WildReportDetailData(
id: 32,
@ -1349,7 +1643,11 @@ private func makeReportDetailData(
longitude: "119.4403263",
handleStatus: handleStatus,
handleStatusText: handleStatusText,
handleRemark: "处理备注",
handleRemark: handleRemark,
handledAt: handledAt,
handler: handler,
handlerName: handlerName,
handlerPhone: handlerPhone,
evidenceSummary: WildReportEvidenceSummary(imageCount: 1, videoCount: 1),
evidences: [
WildReportDetailEvidence(
@ -1365,6 +1663,7 @@ private func makeReportDetailData(
fileName: "report.mov"
)
],
handleEvidences: handleEvidences,
paymentScreenshots: [
WildReportDetailEvidence(
fileType: 5,