Files
suixinkan_ios_uikit/suixinkan_iosTests/OrderLongTailTests.swift
2026-06-26 14:33:31 +08:00

597 lines
28 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.

//
// 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])
}