Add TravelAlbum, ProfileSpace, and wired camera transfer modules.

Introduce travel album entry with Sony PTP tethering pipeline, profile space settings page, home routing updates, Launch Screen storyboard, and related tests/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 09:41:05 +08:00
parent 5692134efc
commit d2fe5d71e4
48 changed files with 6477 additions and 11 deletions

View File

@ -0,0 +1,100 @@
//
// TravelAlbumAPITests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/29.
//
import XCTest
@testable import suixinkan
@MainActor
/// API pathquery body
final class TravelAlbumAPITests: XCTestCase {
/// path
func testAlbumListAndCreateUseExpectedPaths() async throws {
let session = TravelAlbumRecordingURLSession(responses: [Self.listResponse, Self.createResponse])
let api = TravelAlbumAPI(client: APIClient(session: session))
let list = try await api.albumList(page: 1, pageSize: 20)
_ = try await api.createAlbum(
TravelAlbumCreateRequest(
name: "2026-06-29-001",
type: 1,
orderNumber: nil,
materialNum: 3,
materialPrice: 9.9,
materialPackagePrice: 99,
photoPrice: 0
)
)
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/yf-handset-app/photog/travel-album/list",
"/api/yf-handset-app/photog/travel-album/create"
])
XCTAssertEqual(list.total, 1)
XCTAssertEqual(list.list.first?.name, "2026-06-29-001")
let createBody = try bodyObject(from: session.requests[1])
XCTAssertEqual(createBody["name"] as? String, "2026-06-29-001")
XCTAssertEqual(createBody["type"] as? Int, 1)
}
/// body
func testUploadMaterialUsesExpectedBody() async throws {
let session = TravelAlbumRecordingURLSession(data: Self.uploadMaterialResponse)
let api = TravelAlbumAPI(client: APIClient(session: session))
_ = try await api.uploadMaterial(
TravelAlbumUploadMaterialRequest(
userEquityTravelId: 8,
fileName: "DSC_001.JPG",
fileURL: "https://cdn/a.jpg"
)
)
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/travel-album/upload-material")
let body = try bodyObject(from: try XCTUnwrap(session.requests.first))
XCTAssertEqual(body["user_equity_travel_id"] as? Int, 8)
XCTAssertEqual(body["file_name"] as? String, "DSC_001.JPG")
XCTAssertEqual(body["file_url"] as? String, "https://cdn/a.jpg")
}
fileprivate static let listResponse = Data(
#"{"code":100000,"msg":"success","data":{"total":"1","list":[{"id":"8","store_user_id":"1","name":"2026-06-29-001","type":"1","order_number":"","material_num":"3","material_price":"990","material_package_price":"9900","photo_price":"0","cover_url":"","user_id":"100","status":"1","created_at":"2026","updated_at":"2026","user":{"id":"200","phone":"13800138000"}}]}}"#.utf8
)
fileprivate static let createResponse = Data(
#"{"code":100000,"msg":"success","data":{"id":"9"}}"#.utf8
)
fileprivate static let uploadMaterialResponse = Data(
#"{"code":100000,"msg":"success","data":{"id":"11","user_equity_travel_id":"8","status":"1","order_number":"","user_id":"100","file_name":"DSC_001.JPG","file_type":"2","file_url":"https://cdn/a.jpg","file_size":"2048","cover_url":"","is_purchased":"0","created_at":"2026","updated_at":"2026"}}"#.utf8
)
}
private final class TravelAlbumRecordingURLSession: URLSessionProtocol {
private var responses: [Data]
private(set) var requests: [URLRequest] = []
init(data: Data) {
responses = [data]
}
init(responses: [Data]) {
self.responses = responses
}
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
let data = responses.isEmpty ? TravelAlbumAPITests.listResponse : responses.removeFirst()
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
return (data, response)
}
}
private func bodyObject(from request: URLRequest) throws -> [String: Any] {
let data = try XCTUnwrap(request.httpBody)
return try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
}

View File

@ -0,0 +1,124 @@
//
// TravelAlbumViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/29.
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class TravelAlbumViewModelTests: XCTestCase {
///
func testCreatePreShootRequiresSinglePrice() async {
let viewModel = TravelAlbumEntryViewModel()
let api = MockTravelAlbumAPI()
let success = await viewModel.confirmCreateTask(
api: api,
mode: TravelAlbumEntryViewModel.modePreShoot,
freeCount: "3",
singlePrice: "",
packagePrice: "99",
order: nil,
scenicSelected: true
)
XCTAssertFalse(success)
XCTAssertEqual(viewModel.errorMessage, "请输入单张照片价格")
XCTAssertEqual(api.createCallCount, 0)
}
///
func testCreatePreOrderRequiresOrder() async {
let viewModel = TravelAlbumEntryViewModel()
let api = MockTravelAlbumAPI()
let success = await viewModel.confirmCreateTask(
api: api,
mode: TravelAlbumEntryViewModel.modePreOrder,
freeCount: "",
singlePrice: "",
packagePrice: "",
order: nil,
scenicSelected: true
)
XCTAssertFalse(success)
XCTAssertEqual(viewModel.errorMessage, "请选择绑定订单")
}
///
func testCreateSuccessReloadsList() async {
let viewModel = TravelAlbumEntryViewModel()
let api = MockTravelAlbumAPI()
let success = await viewModel.confirmCreateTask(
api: api,
mode: TravelAlbumEntryViewModel.modePreShoot,
freeCount: "3",
singlePrice: "9.9",
packagePrice: "99",
order: nil,
scenicSelected: true
)
XCTAssertTrue(success)
XCTAssertEqual(api.createCallCount, 1)
XCTAssertEqual(api.listCallCount, 1)
}
///
func testBuildTodayAlbumNameIncrementsSuffix() {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let today = formatter.string(from: Date())
let name = TravelAlbumEntryViewModel.buildTodayAlbumName(
existingNames: ["\(today)-001", "\(today)-002", "2026-06-28-001"]
)
XCTAssertTrue(name.hasSuffix("003"))
}
}
@MainActor
private final class MockTravelAlbumAPI: TravelAlbumServing {
var createCallCount = 0
var listCallCount = 0
func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }
func createAlbum(_ request: TravelAlbumCreateRequest) async throws -> TravelAlbumCreateResponse {
createCallCount += 1
return TravelAlbumCreateResponse(id: 9)
}
func albumList(page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumItem> {
listCallCount += 1
return ListPayload(total: 0, list: [])
}
func albumInfo(id: Int) async throws -> TravelAlbumItem {
throw APIError.emptyData
}
func editAlbum(_ request: TravelAlbumEditRequest) async throws {}
func deleteAlbum(_ request: TravelAlbumDeleteRequest) async throws {}
func materialList(userEquityTravelId: Int, page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumMaterial> {
ListPayload(total: 0, list: [])
}
func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial {
throw APIError.emptyData
}
func deleteMaterial(_ request: TravelAlbumDeleteMaterialRequest) async throws {}
func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse {
TravelAlbumMpCodeResponse(mpCodeOssURL: "")
}
}