From aca19a53fb041ee1c03e432e7f7daed312d847be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Tue, 11 Aug 2026 15:57:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8=E6=97=85=E6=8B=8D?= =?UTF-8?q?=E7=9B=B8=E5=86=8C=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TravelAlbum/API/TravelAlbumAPI.swift | 19 ++++++++++---- .../TravelAlbumDetailViewModel.swift | 6 ++--- .../TravelAlbumPreviewActionHandler.swift | 2 +- suixinkanTests/TravelAlbumAPITests.swift | 16 ++++++++++++ ...TravelAlbumDetailViewControllerTests.swift | 10 +++---- .../TravelAlbumViewModelTests.swift | 26 ++++++++++++++++--- 6 files changed, 61 insertions(+), 18 deletions(-) diff --git a/suixinkan/Features/TravelAlbum/API/TravelAlbumAPI.swift b/suixinkan/Features/TravelAlbum/API/TravelAlbumAPI.swift index 9933892..b7bda96 100644 --- a/suixinkan/Features/TravelAlbum/API/TravelAlbumAPI.swift +++ b/suixinkan/Features/TravelAlbum/API/TravelAlbumAPI.swift @@ -41,8 +41,8 @@ protocol TravelAlbumServing { /// 删除旅拍相册。 func deleteAlbum(id: Int) async throws - /// 删除旅拍相册素材。 - func deleteMaterial(id: Int) async throws + /// 批量删除旅拍相册素材。 + func batchDeleteMaterials(ids: [Int]) async throws /// 拉取相册小程序码。 func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse @@ -164,10 +164,14 @@ final class TravelAlbumAPI: TravelAlbumServing { ) } - /// 删除旅拍相册素材。 - func deleteMaterial(id: Int) async throws { + /// 批量删除旅拍相册素材。 + func batchDeleteMaterials(ids: [Int]) async throws { let _: EmptyPayload = try await client.send( - APIRequest(method: .post, path: "\(basePath)/delete-material", body: TravelAlbumIDRequest(id: id)) + APIRequest( + method: .post, + path: "\(basePath)/batch-delete-material", + body: TravelAlbumBatchDeleteMaterialRequest(ids: ids) + ) ) } @@ -212,3 +216,8 @@ final class TravelAlbumAPI: TravelAlbumServing { private struct TravelAlbumIDRequest: Encodable, Sendable { let id: Int } + +/// 旅拍相册素材批量删除请求体。 +private struct TravelAlbumBatchDeleteMaterialRequest: Encodable, Sendable { + let ids: [Int] +} diff --git a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift index 816b8c5..3a724b7 100644 --- a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift +++ b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift @@ -267,15 +267,13 @@ final class TravelAlbumDetailViewModel { /// 删除已选素材。 func deleteSelectedMaterials(api: any TravelAlbumServing) async { - let ids = Array(selectedMaterialIds) + let ids = selectedMaterialIds.sorted() guard !ids.isEmpty else { onShowMessage?("请选择要删除的素材") return } do { - for id in ids { - try await api.deleteMaterial(id: id) - } + try await api.batchDeleteMaterials(ids: ids) onShowMessage?("删除成功") isSelectionMode = false selectedMaterialIds = [] diff --git a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumPreviewActionHandler.swift b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumPreviewActionHandler.swift index 4c7d306..4052531 100644 --- a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumPreviewActionHandler.swift +++ b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumPreviewActionHandler.swift @@ -17,7 +17,7 @@ final class TravelAlbumPreviewActionHandler: TravelAlbumPreviewActionHandling { /// 调用素材删除接口,仅在服务端确认成功后返回成功结果。 func deleteProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { do { - try await api.deleteMaterial(id: originalMaterialId) + try await api.batchDeleteMaterials(ids: [originalMaterialId]) return .success("删除成功") } catch is CancellationError { return .failure("删除已取消") diff --git a/suixinkanTests/TravelAlbumAPITests.swift b/suixinkanTests/TravelAlbumAPITests.swift index d9f96a3..482784e 100644 --- a/suixinkanTests/TravelAlbumAPITests.swift +++ b/suixinkanTests/TravelAlbumAPITests.swift @@ -104,6 +104,22 @@ final class TravelAlbumAPITests: XCTestCase { XCTAssertEqual(query?.first { $0.name == "material_id" }?.value, "6") } + func testBatchDeleteMaterialsBuildsPathAndBody() async throws { + let session = MockURLSession(responses: [envelopeJSON("{}")]) + let api = TravelAlbumAPI(client: APIClient(environment: .testing, session: session)) + + try await api.batchDeleteMaterials(ids: [2031, 2032, 2033]) + + let request = try XCTUnwrap(session.requests.first) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual( + request.url?.path, + "/api/yf-handset-app/photog/travel-album/batch-delete-material" + ) + let body = try JSONSerialization.jsonObject(with: try XCTUnwrap(request.httpBody)) as? [String: Any] + XCTAssertEqual(body?["ids"] as? [Int], [2031, 2032, 2033]) + } + func testUploadMaterialBuildsPathAndBody() async throws { let material = envelopeJSON(#"{"id":6,"user_equity_travel_id":3,"status":1,"order_number":"","user_id":9,"file_name":"A.JPG","file_type":1,"file_url":"https://cdn/a.jpg","file_size":1024,"cover_url":"","is_purchased":false,"created_at":"2026-07-08 10:00:00","updated_at":"2026-07-08 10:00:00"}"#) let session = MockURLSession(responses: [material]) diff --git a/suixinkanTests/TravelAlbumDetailViewControllerTests.swift b/suixinkanTests/TravelAlbumDetailViewControllerTests.swift index b25e804..d113aab 100644 --- a/suixinkanTests/TravelAlbumDetailViewControllerTests.swift +++ b/suixinkanTests/TravelAlbumDetailViewControllerTests.swift @@ -57,13 +57,13 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { let success = await handler.deleteProject(originalMaterialId: 8) - XCTAssertEqual(api.deletedMaterialIds, [8]) + XCTAssertEqual(api.deletedMaterialIDBatches, [[8]]) XCTAssertEqual(success, .success("删除成功")) api.deleteMaterialError = APIError.serverCode(500, "服务暂不可用") let failure = await handler.deleteProject(originalMaterialId: 9) - XCTAssertEqual(api.deletedMaterialIds, [8, 9]) + XCTAssertEqual(api.deletedMaterialIDBatches, [[8], [9]]) XCTAssertEqual(failure, .failure("服务暂不可用")) } @@ -92,7 +92,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { controller.view.allAccessibilityLabels().contains("第 1 张,共 2 张") } - XCTAssertEqual(api.deletedMaterialIds, [1]) + XCTAssertEqual(api.deletedMaterialIDBatches, [[1]]) XCTAssertEqual(deletedIds, [1]) XCTAssertTrue( controller.view.allLabels().contains { $0.text == "第二张.jpg" } @@ -120,7 +120,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { controller.view.allAccessibilityLabels().contains("第 2 张,共 2 张") } - XCTAssertEqual(api.deletedMaterialIds, [3]) + XCTAssertEqual(api.deletedMaterialIDBatches, [[3]]) XCTAssertTrue( controller.view.allLabels().contains { $0.text == "第二张.jpg" } ) @@ -146,7 +146,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { controller.deleteCurrentProjectAfterConfirmation() await waitUntil { host.presentedViewController == nil } - XCTAssertEqual(api.deletedMaterialIds, [1]) + XCTAssertEqual(api.deletedMaterialIDBatches, [[1]]) XCTAssertNil(host.presentedViewController) window.isHidden = true } diff --git a/suixinkanTests/TravelAlbumViewModelTests.swift b/suixinkanTests/TravelAlbumViewModelTests.swift index 7cdd912..65b9227 100644 --- a/suixinkanTests/TravelAlbumViewModelTests.swift +++ b/suixinkanTests/TravelAlbumViewModelTests.swift @@ -242,6 +242,26 @@ final class TravelAlbumDetailViewModelTests: XCTestCase { XCTAssertEqual(api.deletedAlbumIds, [5]) XCTAssertEqual(deletedId, 5) } + + func testDeleteSelectedMaterialsUsesSingleBatchRequest() async { + let api = TravelAlbumMockAPI() + api.materialListResponses = [ + TravelAlbumListResponse(total: 0, list: []), + TravelAlbumListResponse(total: 0, list: []), + TravelAlbumListResponse(total: 0, list: []), + ] + let viewModel = TravelAlbumDetailViewModel(albumId: 5) + viewModel.toggleSelectionMode() + viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 2033, status: 1)) + viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 2031, status: 1)) + viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 2032, status: 1)) + + await viewModel.deleteSelectedMaterials(api: api) + + XCTAssertEqual(api.deletedMaterialIDBatches, [[2031, 2032, 2033]]) + XCTAssertFalse(viewModel.isSelectionMode) + XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty) + } } /// 有线传输 ViewModel 测试。 @@ -1097,7 +1117,7 @@ final class TravelAlbumMockAPI: TravelAlbumServing { private(set) var uploadMaterialRequests: [TravelAlbumUploadMaterialRequest] = [] private(set) var materialClientPhotoIdsCallCount = 0 private(set) var deletedAlbumIds: [Int] = [] - private(set) var deletedMaterialIds: [Int] = [] + private(set) var deletedMaterialIDBatches: [[Int]] = [] private(set) var aiRetouchTemplateScenicIds: [Int] = [] private(set) var aiRetouchRequests: [TravelAlbumAIRetouchRequest] = [] private(set) var aiReretouchRequests: [TravelAlbumAIReretouchRequest] = [] @@ -1167,8 +1187,8 @@ final class TravelAlbumMockAPI: TravelAlbumServing { deletedAlbumIds.append(id) } - func deleteMaterial(id: Int) async throws { - deletedMaterialIds.append(id) + func batchDeleteMaterials(ids: [Int]) async throws { + deletedMaterialIDBatches.append(ids) if deleteMaterialDelayNanoseconds > 0 { try await Task.sleep(nanoseconds: deleteMaterialDelayNanoseconds) }