Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,596 @@
//
// OrderLongTailTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/25.
//
import XCTest
@testable import suixinkan_ios
@MainActor
/// 退 API ViewModel
final class OrderLongTailTests: XCTestCase {
/// 使
func testDepositOrderAPIRequestsAndDecodesLossyFields() async throws {
let session = LongTailRecordingURLSession(responses: [
try TestFixture.data(named: "deposit_order_list_success"),
try TestFixture.data(named: "empty_success"),
try TestFixture.data(named: "empty_success")
])
let api = OrdersAPI(client: APIClient(session: session))
let list = try await api.depositOrderList(scenicId: 88, page: 0, pageSize: 0)
try await api.depositOrderWriteOff(orderNumber: "DEP001")
try await api.depositOrderRefund(orderNumber: "DEP002", refundReason: "游客取消行程")
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/yf-handset-app/photog/order/deposit-list",
"/api/yf-handset-app/photog/order/deposit-writeoff",
"/api/yf-handset-app/photog/order/deposit-refund"
])
let query = longTailQueryItems(from: try XCTUnwrap(session.requests.first))
XCTAssertEqual(query["scenic_id"], "88")
XCTAssertEqual(query["page"], "1")
XCTAssertEqual(query["page_size"], "1")
XCTAssertEqual(list.total, 2)
XCTAssertEqual(list.list[1].id, 4102)
XCTAssertEqual(list.list[1].userPhone, "13900000000")
let writeOffBody = try longTailBodyObject(from: session.requests[1])
XCTAssertEqual(writeOffBody["order_number"] as? String, "DEP001")
let refundBody = try longTailBodyObject(from: session.requests[2])
XCTAssertEqual(refundBody["order_number"] as? String, "DEP002")
XCTAssertEqual(refundBody["refund_reason"] as? String, "游客取消行程")
}
/// 退 pathquerybody
func testShootingRefundAndHistoryAPIRequestsAndDecodes() async throws {
let session = LongTailRecordingURLSession(responses: [
try TestFixture.data(named: "store_order_shooting_detail_success"),
try TestFixture.data(named: "multi_travel_shoot_history_success"),
try TestFixture.data(named: "empty_success")
])
let api = OrdersAPI(client: APIClient(session: session))
let shooting = try await api.storeOrderShootingDetail(
storeId: 66,
orderNumber: "DEP001",
scenicSpotId: 901,
photogUid: 42
)
let history = try await api.multiTravelShootHistory(orderNumber: "MT001")
try await api.orderRefund(
orderNumber: "STORE001",
refundType: .partial,
refundAmount: "10.50",
refundReason: "游客取消部分项目"
)
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/app/store/order/shooting-detail",
"/api/yf-handset-app/photog/order/multi-travel/shoot-history",
"/api/yf-handset-app/photog/order/refund"
])
let shootingQuery = longTailQueryItems(from: session.requests[0])
XCTAssertEqual(shootingQuery["store_id"], "66")
XCTAssertEqual(shootingQuery["order_number"], "DEP001")
XCTAssertEqual(shootingQuery["scenic_spot_id"], "901")
XCTAssertEqual(shootingQuery["photog_uid"], "42")
XCTAssertEqual(shooting.orderComment?.starShooting, 5)
XCTAssertEqual(shooting.materialList.first?.fileSize, 10_485_760)
XCTAssertTrue(shooting.materialList.first?.isVideo == true)
XCTAssertEqual(longTailQueryItems(from: session.requests[1])["order_number"], "MT001")
XCTAssertEqual(history.projectType, 19)
XCTAssertEqual(history.photogSpotList.first?.photographerDisplayName, "小景")
XCTAssertEqual(history.photogSpotList.first?.files.count, 2)
let refundBody = try longTailBodyObject(from: session.requests[2])
XCTAssertEqual(refundBody["order_number"] as? String, "STORE001")
XCTAssertEqual(refundBody["refund_type"] as? Int, OrderRefundMode.partial.rawValue)
XCTAssertEqual(refundBody["refund_amount"] as? String, "10.50")
XCTAssertNil(refundBody["store_id"])
}
/// pathquerybody
func testMultiTravelTaskUploadAPIRequestsAndDecodes() async throws {
let session = LongTailRecordingURLSession(responses: [
try TestFixture.data(named: "multi_travel_verified_spots_success"),
try TestFixture.data(named: "empty_success")
])
let api = OrdersAPI(client: APIClient(session: session))
let spots = try await api.multiTravelVerifiedScenicSpotList(orderNumber: "MT001")
try await api.multiTravelUploadMaterial(
MultiTravelUploadMaterialRequest(
orderNumber: "MT001",
scenicSpotId: 901,
cloudFile: [MultiTravelCloudFileItem(fileId: 7001)],
uploadFile: [MultiTravelUploadFileItem(fileName: "spot.jpg", fileUrl: "https://cdn.example.com/spot.jpg")]
)
)
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/yf-handset-app/photog/order/multi-travel/verified-scenic-spot-list",
"/api/yf-handset-app/photog/order/multi-travel/upload-material"
])
XCTAssertEqual(longTailQueryItems(from: session.requests[0])["order_number"], "MT001")
XCTAssertEqual(spots.map(\.id), [901, 902])
XCTAssertEqual(spots.last?.name, "观景台")
let body = try longTailBodyObject(from: session.requests[1])
XCTAssertEqual(body["order_number"] as? String, "MT001")
XCTAssertEqual(body["scenic_spot_id"] as? Int, 901)
XCTAssertEqual((body["cloud_file"] as? [[String: Any]])?.first?["file_id"] as? Int, 7001)
XCTAssertEqual((body["upload_file"] as? [[String: Any]])?.first?["file_url"] as? String, "https://cdn.example.com/spot.jpg")
}
///
func testDepositListClearsWithoutScenicAndStopsAtLastPage() async throws {
let service = LongTailMockOrderService()
service.depositListResponses = [try Self.depositPayload()]
let viewModel = DepositOrderListViewModel()
await viewModel.reload(api: service, scenicId: 88, reset: true)
await viewModel.reload(api: service, scenicId: 88, reset: false)
await viewModel.reload(api: service, scenicId: nil, reset: true)
XCTAssertTrue(viewModel.orders.isEmpty)
XCTAssertEqual(viewModel.total, 0)
XCTAssertEqual(viewModel.page, 1)
XCTAssertEqual(service.depositListCalls, [
.init(scenicId: 88, page: 1, pageSize: 10)
])
}
/// 退
func testDepositWriteOffAndRefundRefreshOrKeepListOnFailure() async throws {
let service = LongTailMockOrderService()
service.depositListResponses = [
try Self.depositPayload(),
try Self.depositPayload(),
try Self.depositPayload()
]
let viewModel = DepositOrderListViewModel()
await viewModel.reload(api: service, scenicId: 88, reset: true)
let writeOffSuccess = await viewModel.writeOff(api: service, scenicId: 88, orderNumber: "DEP001")
let emptyReason = await viewModel.refund(api: service, scenicId: 88, orderNumber: "DEP001", reason: " ")
service.depositRefundError = APIError.serverCode(500, "退款失败")
let refundFailure = await viewModel.refund(api: service, scenicId: 88, orderNumber: "DEP001", reason: "游客取消")
XCTAssertTrue(writeOffSuccess)
XCTAssertFalse(emptyReason)
XCTAssertFalse(refundFailure)
XCTAssertEqual(service.depositWriteOffNumbers, ["DEP001"])
XCTAssertEqual(service.depositRefundCalls, [.init(orderNumber: "DEP001", reason: "游客取消")])
XCTAssertEqual(service.depositListCalls.count, 2)
XCTAssertEqual(viewModel.orders.count, 2)
XCTAssertNil(viewModel.operatingOrderNumber)
XCTAssertEqual(viewModel.errorMessage, "退款失败")
}
///
func testDepositDetailAndShootingSkipWithoutStore() async {
let service = LongTailMockOrderService()
let detailViewModel = DepositOrderDetailViewModel()
let shootingViewModel = DepositOrderShootingInfoViewModel()
await detailViewModel.load(api: service, storeId: nil, orderNumber: "DEP001")
await shootingViewModel.load(api: service, storeId: nil, orderNumber: "DEP001", scenicSpotId: 901, photogUid: 42)
XCTAssertTrue(service.storeDetailCalls.isEmpty)
XCTAssertTrue(service.storeShootingCalls.isEmpty)
XCTAssertEqual(detailViewModel.errorMessage, "当前账号缺少门店信息")
XCTAssertEqual(shootingViewModel.errorMessage, "当前账号缺少门店信息")
}
/// 退
func testOrderRefundValidationAndSubmit() async throws {
let service = LongTailMockOrderService()
let viewModel = OrderRefundViewModel()
let refundable = try Self.orderEntity(orderStatus: 18, orderType: 4, actualPayAmount: "99.50")
let unavailable = try Self.orderEntity(orderStatus: 50, orderType: 4, actualPayAmount: "99.50")
XCTAssertTrue(viewModel.canRefund(refundable))
XCTAssertFalse(viewModel.canRefund(unavailable))
viewModel.begin(item: refundable)
XCTAssertEqual(viewModel.amount, "99.50")
let missingReasonSuccess = await viewModel.submit(api: service, item: refundable)
XCTAssertFalse(missingReasonSuccess)
XCTAssertEqual(viewModel.errorMessage, "请输入退款原因")
viewModel.reason = "部分退款"
viewModel.mode = .partial
viewModel.amount = "12.345"
let invalidAmountSuccess = await viewModel.submit(api: service, item: refundable)
XCTAssertFalse(invalidAmountSuccess)
XCTAssertEqual(viewModel.errorMessage, "请输入有效的退款金额")
viewModel.amount = "120.00"
let overLimitSuccess = await viewModel.submit(api: service, item: refundable)
XCTAssertFalse(overLimitSuccess)
XCTAssertEqual(viewModel.errorMessage, "退款金额不能大于可退金额")
viewModel.amount = "12.3"
let submitSuccess = await viewModel.submit(api: service, item: refundable)
XCTAssertTrue(submitSuccess)
XCTAssertEqual(service.orderRefundCalls, [
.init(orderNumber: "STORE001", mode: .partial, amount: "12.30", reason: "部分退款")
])
XCTAssertNil(viewModel.errorMessage)
XCTAssertFalse(viewModel.submitting)
}
///
func testHistoricalShootingLoadSuccessAndFailure() async throws {
let service = LongTailMockOrderService()
service.historyResponses = [try TestFixture.payload(MultiTravelShootHistoryResponse.self, named: "multi_travel_shoot_history_success")]
let viewModel = HistoricalShootingInfoViewModel()
await viewModel.load(api: service, orderNumber: " ")
XCTAssertTrue(service.historyCalls.isEmpty)
XCTAssertEqual(viewModel.errorMessage, "订单号不能为空")
await viewModel.load(api: service, orderNumber: "MT001")
XCTAssertEqual(viewModel.projectName, "多点旅拍")
XCTAssertEqual(viewModel.spots.first?.scenicSpotName, "湖心亭")
XCTAssertEqual(viewModel.spots.first?.files.count, 2)
service.historyError = APIError.serverCode(500, "历史拍摄失败")
await viewModel.load(api: service, orderNumber: "MT001")
XCTAssertTrue(viewModel.spots.isEmpty)
XCTAssertEqual(viewModel.errorMessage, "历史拍摄失败")
}
///
func testMultiTravelTaskUploadLoadsAndRepairsSpotSelection() async throws {
let service = LongTailMockOrderService()
service.verifiedSpotResponses = [
try Self.verifiedSpots(),
[try Self.verifiedSpot(id: 902, name: "观景台")]
]
let viewModel = MultiTravelTaskUploadViewModel()
await viewModel.loadSpots(api: service)
XCTAssertTrue(service.verifiedSpotCalls.isEmpty)
viewModel.orderNumber = "MT001"
await viewModel.loadSpots(api: service)
XCTAssertEqual(viewModel.selectedSpotId, 901)
viewModel.selectSpot(id: 901)
await viewModel.loadSpots(api: service)
XCTAssertEqual(viewModel.selectedSpotId, 902)
service.verifiedSpotError = APIError.serverCode(500, "打卡点失败")
await viewModel.loadSpots(api: service)
XCTAssertTrue(viewModel.spots.isEmpty)
XCTAssertNil(viewModel.selectedSpotId)
XCTAssertEqual(viewModel.errorMessage, "打卡点失败")
}
///
func testMultiTravelTaskUploadValidatesAndSubmitsCloudFiles() async throws {
let service = LongTailMockOrderService()
service.verifiedSpotResponses = [try Self.verifiedSpots()]
let uploader = LongTailMockUploader()
let viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: "MT001")
let missingScenicSuccess = await viewModel.submit(api: service, uploadService: uploader, scenicId: nil)
XCTAssertFalse(missingScenicSuccess)
XCTAssertEqual(viewModel.errorMessage, "请先选择景区")
await viewModel.loadSpots(api: service)
let missingFileSuccess = await viewModel.submit(api: service, uploadService: uploader, scenicId: 88)
XCTAssertFalse(missingFileSuccess)
XCTAssertEqual(viewModel.errorMessage, "请至少选择一个素材文件")
viewModel.mergeCloudFiles([TaskCloudSelectionItem(id: 7001, fileName: "cloud.jpg", fileType: 2, remark: "")])
let submitSuccess = await viewModel.submit(api: service, uploadService: uploader, scenicId: 88)
XCTAssertTrue(submitSuccess)
XCTAssertTrue(viewModel.didSubmitSuccessfully)
XCTAssertEqual(service.uploadMaterialRequests, [
MultiTravelUploadMaterialRequest(
orderNumber: "MT001",
scenicSpotId: 901,
cloudFile: [MultiTravelCloudFileItem(fileId: 7001)],
uploadFile: []
)
])
XCTAssertTrue(uploader.uploadTaskFiles.isEmpty)
}
/// OSS
func testMultiTravelTaskUploadUploadsLocalFilesBeforeSubmit() async throws {
let service = LongTailMockOrderService()
service.verifiedSpotResponses = [try Self.verifiedSpots()]
let uploader = LongTailMockUploader()
uploader.uploadTaskFileURLs = ["https://cdn.example.com/local.jpg"]
let viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: "MT001")
await viewModel.loadSpots(api: service)
viewModel.addLocalFile(data: Data([1, 2, 3]), fileName: "local.jpg")
let success = await viewModel.submit(api: service, uploadService: uploader, scenicId: 88)
XCTAssertTrue(success)
XCTAssertEqual(uploader.uploadTaskFiles.map(\.fileName), ["local.jpg"])
XCTAssertEqual(service.uploadMaterialRequests.first?.uploadFile, [
MultiTravelUploadFileItem(fileName: "local.jpg", fileUrl: "https://cdn.example.com/local.jpg")
])
let failingService = LongTailMockOrderService()
failingService.verifiedSpotResponses = [try Self.verifiedSpots()]
let failingUploader = LongTailMockUploader()
failingUploader.uploadError = APIError.serverCode(500, "上传失败")
let failingViewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: "MT001")
await failingViewModel.loadSpots(api: failingService)
failingViewModel.addLocalFile(data: Data([1]), fileName: "bad.mp4")
let failure = await failingViewModel.submit(api: failingService, uploadService: failingUploader, scenicId: 88)
XCTAssertFalse(failure)
XCTAssertTrue(failingService.uploadMaterialRequests.isEmpty)
XCTAssertEqual(failingViewModel.errorMessage, "上传失败")
}
/// fixture
private static func depositPayload() throws -> ListPayload<DepositOrderListItem> {
try TestFixture.payload(ListPayload<DepositOrderListItem>.self, named: "deposit_order_list_success")
}
/// 退
private static func orderEntity(orderStatus: Int, orderType: Int, actualPayAmount: String) throws -> OrderEntity {
let json = """
{
"order_number": "STORE001",
"order_status": "\(orderStatus)",
"order_type": "\(orderType)",
"actual_pay_amount": "\(actualPayAmount)",
"actual_refund_amount": "0",
"refund_amount": "0",
"order_type_label": "",
"order_status_name": "退"
}
"""
return try JSONDecoder().decode(OrderEntity.self, from: Data(json.utf8))
}
/// fixture
private static func verifiedSpots() throws -> [MultiTravelVerifiedScenicSpotItem] {
try TestFixture.payload([MultiTravelVerifiedScenicSpotItem].self, named: "multi_travel_verified_spots_success")
}
///
private static func verifiedSpot(id: Int, name: String) throws -> MultiTravelVerifiedScenicSpotItem {
let json = #"{"id":"\#(id)","name":"\#(name)"}"#
return try JSONDecoder().decode(MultiTravelVerifiedScenicSpotItem.self, from: Data(json.utf8))
}
}
/// URLSession
private final class LongTailRecordingURLSession: URLSessionProtocol {
private var responses: [Data]
private(set) var requests: [URLRequest] = []
init(responses: [Data]) {
self.responses = responses
}
///
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
let data = responses.isEmpty ? Data(#"{"code":100000,"msg":"success"}"#.utf8) : responses.removeFirst()
return (
data,
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
)
}
}
@MainActor
/// 退
private final class LongTailMockOrderService: OrderServing {
struct DepositListCall: Equatable {
let scenicId: Int
let page: Int
let pageSize: Int
}
struct DepositRefundCall: Equatable {
let orderNumber: String
let reason: String
}
struct OrderRefundCall: Equatable {
let orderNumber: String
let mode: OrderRefundMode
let amount: String
let reason: String
}
var depositListResponses: [ListPayload<DepositOrderListItem>] = []
var detailResponses: [StoreOrderDetailResponse] = []
var shootingResponses: [StoreOrderShootingDetailResponse] = []
var historyResponses: [MultiTravelShootHistoryResponse] = []
var verifiedSpotResponses: [[MultiTravelVerifiedScenicSpotItem]] = []
var depositWriteOffError: Error?
var depositRefundError: Error?
var orderRefundError: Error?
var historyError: Error?
var verifiedSpotError: Error?
var uploadMaterialError: Error?
private(set) var depositListCalls: [DepositListCall] = []
private(set) var depositWriteOffNumbers: [String] = []
private(set) var depositRefundCalls: [DepositRefundCall] = []
private(set) var orderRefundCalls: [OrderRefundCall] = []
private(set) var storeDetailCalls: [(storeId: Int, orderNumber: String)] = []
private(set) var storeShootingCalls: [(storeId: Int, orderNumber: String, scenicSpotId: Int, photogUid: Int)] = []
private(set) var historyCalls: [String] = []
private(set) var verifiedSpotCalls: [String] = []
private(set) var uploadMaterialRequests: [MultiTravelUploadMaterialRequest] = []
/// 使
func orderList(
scenicId: Int,
page: Int,
pageSize: Int,
orderStatus: Int?,
userPhone: String?,
startTime: String?,
endTime: String?,
isRefined: Int?,
isScenicAdmin: Bool
) async throws -> ListPayload<OrderEntity> {
ListPayload(total: 0, list: [])
}
/// 使
func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
ListPayload(total: 0, list: [])
}
/// 使
func writeOff(orderNumber: String) async throws {}
///
func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse {
storeDetailCalls.append((storeId: storeId, orderNumber: orderNumber))
return detailResponses.isEmpty ? try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success") : detailResponses.removeFirst()
}
///
func depositOrderList(scenicId: Int, page: Int, pageSize: Int) async throws -> ListPayload<DepositOrderListItem> {
depositListCalls.append(.init(scenicId: scenicId, page: page, pageSize: pageSize))
return depositListResponses.isEmpty ? ListPayload(total: 0, list: []) : depositListResponses.removeFirst()
}
///
func depositOrderWriteOff(orderNumber: String) async throws {
depositWriteOffNumbers.append(orderNumber)
if let depositWriteOffError {
throw depositWriteOffError
}
}
/// 退
func depositOrderRefund(orderNumber: String, refundReason: String) async throws {
depositRefundCalls.append(.init(orderNumber: orderNumber, reason: refundReason))
if let depositRefundError {
throw depositRefundError
}
}
///
func storeOrderShootingDetail(
storeId: Int,
orderNumber: String,
scenicSpotId: Int,
photogUid: Int
) async throws -> StoreOrderShootingDetailResponse {
storeShootingCalls.append((storeId: storeId, orderNumber: orderNumber, scenicSpotId: scenicSpotId, photogUid: photogUid))
return shootingResponses.isEmpty ? try TestFixture.payload(StoreOrderShootingDetailResponse.self, named: "store_order_shooting_detail_success") : shootingResponses.removeFirst()
}
/// 退
func orderRefund(orderNumber: String, refundType: OrderRefundMode, refundAmount: String, refundReason: String) async throws {
orderRefundCalls.append(.init(orderNumber: orderNumber, mode: refundType, amount: refundAmount, reason: refundReason))
if let orderRefundError {
throw orderRefundError
}
}
///
func multiTravelShootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
historyCalls.append(orderNumber)
if let historyError {
throw historyError
}
return historyResponses.isEmpty ? try TestFixture.payload(MultiTravelShootHistoryResponse.self, named: "multi_travel_shoot_history_success") : historyResponses.removeFirst()
}
///
func multiTravelVerifiedScenicSpotList(orderNumber: String) async throws -> [MultiTravelVerifiedScenicSpotItem] {
verifiedSpotCalls.append(orderNumber)
if let verifiedSpotError {
throw verifiedSpotError
}
return verifiedSpotResponses.isEmpty ? [] : verifiedSpotResponses.removeFirst()
}
///
func multiTravelUploadMaterial(_ request: MultiTravelUploadMaterialRequest) async throws {
uploadMaterialRequests.append(request)
if let uploadMaterialError {
throw uploadMaterialError
}
}
}
@MainActor
/// OSS
private final class LongTailMockUploader: OSSUploadServing {
struct UploadTaskFileCall: Equatable {
let fileName: String
let fileType: Int
let scenicId: Int
let dataSize: Int
}
var uploadTaskFileURLs: [String] = []
var uploadError: Error?
private(set) var uploadTaskFiles: [UploadTaskFileCall] = []
/// URL
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
uploadTaskFiles.append(.init(fileName: fileName, fileType: fileType, scenicId: scenicId, dataSize: data.count))
if let uploadError {
throw uploadError
}
onProgress(100)
return uploadTaskFileURLs.isEmpty ? "https://cdn.example.com/\(fileName)" : uploadTaskFileURLs.removeFirst()
}
/// 使
func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
/// 使
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
}
/// query 便
private func longTailQueryItems(from request: URLRequest) -> [String: String] {
guard let url = request.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return [:]
}
return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in
item.value.map { (item.name, $0) }
})
}
/// JSON body 便
private func longTailBodyObject(from request: URLRequest) throws -> [String: Any] {
let data = try XCTUnwrap(request.httpBody)
let object = try JSONSerialization.jsonObject(with: data)
return try XCTUnwrap(object as? [String: Any])
}