模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -146,6 +146,77 @@ final class APIClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送单文件 multipart/form-data 请求,并返回解包后的业务数据。
|
||||
func sendMultipart<Response: Decodable>(
|
||||
path: String,
|
||||
fieldName: String = "file",
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
data fileData: Data,
|
||||
timeoutInterval: TimeInterval = 60,
|
||||
tokenOverride: String? = nil
|
||||
) async throws -> Response {
|
||||
let apiPath = path.hasPrefix("/") ? path : "/" + path
|
||||
guard let url = URL(
|
||||
string: environment.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + apiPath
|
||||
) else {
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
var body = Data()
|
||||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
body.append(
|
||||
"Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(fileName)\"\r\n"
|
||||
.data(using: .utf8)!
|
||||
)
|
||||
body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
|
||||
body.append(fileData)
|
||||
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = HTTPMethod.post.rawValue
|
||||
request.timeoutInterval = timeoutInterval
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue(appVersion, forHTTPHeaderField: "X-APP-VERSION")
|
||||
request.setValue(osType, forHTTPHeaderField: "X-OS-TYPE")
|
||||
request.setValue(String(body.count), forHTTPHeaderField: "Content-Length")
|
||||
|
||||
let token = tokenOverride ?? authTokenProvider?()
|
||||
if let token = token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty {
|
||||
request.setValue(token, forHTTPHeaderField: "token")
|
||||
}
|
||||
request.httpBody = body
|
||||
logRequest(request)
|
||||
|
||||
let responseData: Data
|
||||
let response: URLResponse
|
||||
do {
|
||||
(responseData, response) = try await session.data(for: request)
|
||||
} catch is CancellationError {
|
||||
logCancelled(for: request, reason: "CancellationError")
|
||||
throw CancellationError()
|
||||
} catch let error as URLError {
|
||||
if error.code == .cancelled {
|
||||
logCancelled(for: request, reason: "URLError.cancelled")
|
||||
throw CancellationError()
|
||||
}
|
||||
throw APIError.networkFailed(networkErrorMessage(for: error))
|
||||
} catch {
|
||||
throw APIError.networkFailed(error.localizedDescription)
|
||||
}
|
||||
|
||||
logResponse(for: request, response: response, data: responseData)
|
||||
do {
|
||||
try validateHTTPResponse(response, data: responseData)
|
||||
return try decodeEnvelope(Response.self, from: responseData)
|
||||
} catch let error as APIError {
|
||||
notifySessionExpiredIfNeeded(for: error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// 将业务请求模型转换为 URLRequest,并注入公共 Header、token 和请求体。
|
||||
private func makeURLRequest<Response: Decodable>(
|
||||
_ apiRequest: APIRequest<Response>,
|
||||
|
||||
Reference in New Issue
Block a user