Add sample management and upload flows to Assets module.

Extend media library for sample kind with project selection, wire home routing for sample entries, and expand Assets tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 10:06:16 +08:00
parent 607130ade0
commit c772b25897
13 changed files with 523 additions and 111 deletions

View File

@ -76,7 +76,7 @@ final class AssetsAPITests: XCTestCase {
let api = AssetsAPI(client: APIClient(session: session))
let list = try await api.mediaAlbumList(kind: .material, keyword: "航拍", status: 1, page: 0, pageSize: 0)
let detail = try await api.mediaAlbumDetail(id: 7)
let detail = try await api.mediaAlbumDetail(id: 7, kind: .material)
XCTAssertEqual(session.requests.first?.url?.path, "/api/app/media-album/list")
let query = assetsQueryItems(from: try XCTUnwrap(session.requests.first))
@ -87,6 +87,7 @@ final class AssetsAPITests: XCTestCase {
XCTAssertEqual(list.order?.totalNum, 12)
XCTAssertEqual(list.list.first?.likesCount, 3)
XCTAssertEqual(session.requests.last?.url?.path, "/api/app/media-album/detail")
XCTAssertEqual(assetsQueryItems(from: try XCTUnwrap(session.requests.last))["type"], "1")
XCTAssertEqual(detail.mediaList.first?.size, 4096)
XCTAssertTrue(detail.mediaList.first?.isVideo == true)
}
@ -108,8 +109,8 @@ final class AssetsAPITests: XCTestCase {
projectId: 0
)
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 1, listingStatus: 1))
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 1))
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 1, type: 1, listingStatus: 1))
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 1, type: 1))
try await api.mediaAlbumAddTag(MediaAlbumAddTagRequest(name: "航拍"))
try await api.mediaAlbumUpload(upload)
try await api.mediaAlbumEdit(MediaAlbumEditRequest(id: 2, name: "素材", type: 1, mediaType: 2, mediaList: upload.mediaList, coverUrl: upload.coverUrl, coverSize: upload.coverSize, scenicSpotId: 6, description: "描述", materialTag: "航拍"))
@ -126,6 +127,52 @@ final class AssetsAPITests: XCTestCase {
XCTAssertEqual((body["media_list"] as? [[String: Any]])?.first?["oss_url"] as? String, "https://cdn/a.jpg")
}
/// type=2
func testSampleRequestsAndProjectListUseExpectedTypeAndQuery() async throws {
let session = AssetsRecordingURLSession(responses: [
Self.mediaListResponse,
Self.mediaDetailResponse,
Self.emptyResponse,
Self.emptyResponse,
Self.projectListResponse,
Self.emptyResponse
])
let api = AssetsAPI(client: APIClient(session: session))
let upload = MediaAlbumUploadRequest(
name: "亲子样片",
type: 2,
mediaType: 2,
mediaList: [MediaAlbumUploadItem(originalName: "sample.jpg", ossUrl: "https://cdn/sample.jpg", size: 10, fileWidthSize: MediaAlbumFileSize(width: 100, height: 80))],
coverUrl: "https://cdn/cover.jpg",
coverSize: MediaAlbumFileSize(width: 100, height: 80),
scenicSpotId: 6,
description: "描述",
materialTag: "",
projectId: 88
)
_ = try await api.mediaAlbumList(kind: .sample, keyword: "样片", status: nil, page: 1, pageSize: 10)
_ = try await api.mediaAlbumDetail(id: 7, kind: .sample)
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 7, type: 2, listingStatus: 1))
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 7, type: 2))
let projects = try await api.projectList(scenicId: 9, name: "亲子", page: 0, pageSize: 0)
try await api.mediaAlbumUpload(upload)
XCTAssertEqual(assetsQueryItems(from: session.requests[0])["type"], "2")
XCTAssertEqual(assetsQueryItems(from: session.requests[1])["type"], "2")
XCTAssertEqual((try bodyObject(from: session.requests[2]))["type"] as? Int, 2)
XCTAssertEqual((try bodyObject(from: session.requests[3]))["type"] as? Int, 2)
XCTAssertEqual(session.requests[4].url?.path, "/api/yf-handset-app/photog/project/list")
let projectQuery = assetsQueryItems(from: session.requests[4])
XCTAssertEqual(projectQuery["scenic_id"], "9")
XCTAssertEqual(projectQuery["name"], "亲子")
XCTAssertEqual(projectQuery["page"], "1")
XCTAssertEqual(projectQuery["page_size"], "1")
XCTAssertEqual(projects.list.first?.id, 88)
XCTAssertEqual(projects.list.first?.name, "亲子旅拍")
XCTAssertEqual((try bodyObject(from: session.requests[5]))["project_id"] as? Int, 88)
}
/// 使query
func testAlbumRequestsUseExpectedPathQueryAndBodies() async throws {
let session = AssetsRecordingURLSession(responses: [
@ -205,6 +252,11 @@ final class AssetsAPITests: XCTestCase {
{"id":"9","original_name":"v.mp4","oss_url":"https://cdn/v.mp4","thumbnail_url":"","type":"1","size":"4096","show_url":"https://cdn/v.mp4"}
]}}
""".data(using: .utf8)!
fileprivate static let projectListResponse = """
{"code":100000,"msg":"success","data":{"total":"1","list":[
{"id":"88","type":"19","type_name":"多点旅拍","status":"1","status_name":"上架","name":"亲子旅拍","cover_project":"https://cdn/p.jpg","cover_video":"","price":"299","ot_price":"399","price_deposit":"99"}
]}}
""".data(using: .utf8)!
fileprivate static let albumFolderListResponse = """
{"code":100000,"msg":"success","data":{"total":"1","list":[
{"id":"3","name":"旅拍","cover_file_id":"11","count_video":"2","count_image":"4","cover":{"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""},"created_at":"2026","remark":"备注"}

View File

@ -95,6 +95,16 @@ final class AssetsViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
}
/// 使
func testSampleMediaListUsesSampleKind() async {
let api = MockAssetsService()
let viewModel = MediaLibraryViewModel(kind: .sample)
await viewModel.reload(api: api)
XCTAssertEqual(api.mediaListRequests.first?.kind, .sample)
}
///
func testRejectedMediaCannotToggleListing() async {
let api = MockAssetsService()
@ -108,6 +118,18 @@ final class AssetsViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.errorMessage, "审核通过后才能上下架")
}
///
func testSampleToggleListingUsesSampleType() async {
let api = MockAssetsService()
let viewModel = MediaLibraryViewModel(kind: .sample)
let item = MediaLibraryItem.fixture(id: 1, status: 1, listingStatus: 0)
let success = await viewModel.toggleListing(item, api: api)
XCTAssertTrue(success)
XCTAssertEqual(api.mediaOperationRequests.first?.type, 2)
}
/// OSS URL
func testMediaUploadUploadsFilesBeforeSubmit() async {
let api = MockAssetsService()
@ -145,6 +167,61 @@ final class AssetsViewModelTests: XCTestCase {
XCTAssertEqual(api.mediaUploadRequests.count, 0)
}
///
func testSampleUploadRequiresProject() async {
let api = MockAssetsService()
let uploader = MockAssetsUploader()
let viewModel = MediaLibraryEditorViewModel()
viewModel.name = "亲子样片"
viewModel.selectedSpotId = 6
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)
XCTAssertFalse(success)
XCTAssertEqual(viewModel.errorMessage, "请选择关联项目")
XCTAssertEqual(api.mediaUploadRequests.count, 0)
}
///
func testSampleUploadUploadsFilesAndSubmitsProject() async {
let api = MockAssetsService()
let uploader = MockAssetsUploader()
let viewModel = MediaLibraryEditorViewModel()
viewModel.name = "亲子样片"
viewModel.selectedSpotId = 6
viewModel.selectedProjectId = 88
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)
XCTAssertTrue(success)
XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["cover.jpg", "a.jpg"])
XCTAssertEqual(api.mediaUploadRequests.first?.type, 2)
XCTAssertEqual(api.mediaUploadRequests.first?.projectId, 88)
}
///
func testSampleDuplicateSubmitIsIgnored() async {
let api = MockAssetsService()
let uploader = MockAssetsUploader()
let viewModel = MediaLibraryEditorViewModel()
viewModel.name = "亲子样片"
viewModel.selectedSpotId = 6
viewModel.selectedProjectId = 88
viewModel.isSubmitting = true
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)
XCTAssertFalse(success)
XCTAssertEqual(api.mediaUploadRequests.count, 0)
XCTAssertEqual(uploader.uploadedFiles.count, 0)
}
///
func testAlbumListClearsWithoutScenic() async {
let api = MockAssetsService()
@ -274,11 +351,19 @@ private final class MockAssetsService: AssetsServing {
}
struct MediaListRequest: Equatable {
let kind: MediaLibraryKind
let keyword: String
let status: Int?
let page: Int
}
struct ProjectListRequest: Equatable {
let scenicId: Int
let name: String?
let page: Int
let pageSize: Int
}
struct AlbumFolderListRequest: Equatable {
let scenicId: Int
let page: Int
@ -296,6 +381,7 @@ private final class MockAssetsService: AssetsServing {
var cloudPages: [ListPayload<CloudDriveFile>] = [ListPayload(total: 0, list: [])]
var mediaListResponses: [MediaLibraryListResponse] = [MediaLibraryListResponse.fixture(total: 0, ids: [])]
var projectPages: [ListPayload<PhotographerProjectItem>] = [ListPayload(total: 0, list: [])]
var albumFolderPages: [ListPayload<AlbumFolderItem>] = [ListPayload(total: 0, list: [])]
var albumFilePages: [ListPayload<AlbumFileItem>] = [ListPayload(total: 0, list: [])]
var uploadPermission = true
@ -306,6 +392,7 @@ private final class MockAssetsService: AssetsServing {
var mediaOperationRequests: [MediaAlbumOperationRequest] = []
var mediaUploadRequests: [MediaAlbumUploadRequest] = []
var mediaEditRequests: [MediaAlbumEditRequest] = []
var projectListRequests: [ProjectListRequest] = []
var albumFolderListRequests: [AlbumFolderListRequest] = []
var albumInfoRequests: [Int] = []
var albumFileListRequests: [(scenicId: Int, folderId: Int, fileType: Int, page: Int)] = []
@ -347,12 +434,12 @@ private final class MockAssetsService: AssetsServing {
///
func mediaAlbumList(kind: MediaLibraryKind, keyword: String, status: Int?, page: Int, pageSize: Int) async throws -> MediaLibraryListResponse {
mediaListRequests.append(MediaListRequest(keyword: keyword, status: status, page: page))
mediaListRequests.append(MediaListRequest(kind: kind, keyword: keyword, status: status, page: page))
return mediaListResponses.isEmpty ? MediaLibraryListResponse.fixture(total: 0, ids: []) : mediaListResponses.removeFirst()
}
///
func mediaAlbumDetail(id: Int) async throws -> MediaLibraryDetail {
func mediaAlbumDetail(id: Int, kind: MediaLibraryKind) async throws -> MediaLibraryDetail {
MediaLibraryDetail.fixture(id: id)
}
@ -377,6 +464,12 @@ private final class MockAssetsService: AssetsServing {
mediaEditRequests.append(request)
}
///
func projectList(scenicId: Int, name: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
projectListRequests.append(ProjectListRequest(scenicId: scenicId, name: name, page: page, pageSize: pageSize))
return projectPages.isEmpty ? ListPayload(total: 0, list: []) : projectPages.removeFirst()
}
///
func albumFolderList(scenicId: Int, page: Int, pageSize: Int, name: String, startTime: String?, endTime: String?) async throws -> ListPayload<AlbumFolderItem> {
albumFolderListRequests.append(AlbumFolderListRequest(scenicId: scenicId, page: page, pageSize: pageSize, name: name, startTime: startTime, endTime: endTime))