feat: 确认上传
This commit is contained in:
parent
a4890a4d25
commit
a207b7895d
Binary file not shown.
19
wake/Utils/APIConfig.swift
Normal file
19
wake/Utils/APIConfig.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// API 配置信息
|
||||||
|
public enum APIConfig {
|
||||||
|
/// API 基础 URL
|
||||||
|
public static let baseURL = "https://api.memorywake.com/api/v1"
|
||||||
|
|
||||||
|
/// 认证 token - 生产环境中应该存储在 Keychain 中
|
||||||
|
public static let authToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA"
|
||||||
|
|
||||||
|
/// 认证请求头
|
||||||
|
public static var authHeaders: [String: String] {
|
||||||
|
return [
|
||||||
|
"Authorization": "Bearer \(authToken)",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,190 +1,46 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
|
|
||||||
/// 上传管理器,处理图片上传
|
|
||||||
/// 图片上传管理器
|
|
||||||
class ImageUploader: ObservableObject {
|
|
||||||
private let baseURL = "https://api.memorywake.com/api/v1/file/generate-upload-url"
|
|
||||||
|
|
||||||
func uploadImage(_ image: UIImage, completion: @escaping (Result<String, Error>) -> Void) {
|
|
||||||
print("🔄 开始准备上传图片...")
|
|
||||||
// 1. 将图片转换为Data
|
|
||||||
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
|
|
||||||
let error = NSError(domain: "ImageError", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片数据转换失败"])
|
|
||||||
print("❌ 错误:\(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查URL是否有效
|
|
||||||
guard let url = URL(string: baseURL) else {
|
|
||||||
let error = NSError(domain: "URLError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])
|
|
||||||
print("❌ 错误:\(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
||||||
request.setValue("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA", forHTTPHeaderField: "Authorization")
|
|
||||||
|
|
||||||
// 3. 准备请求参数
|
|
||||||
let fileName = "avatar_\(UUID().uuidString).jpg"
|
|
||||||
let parameters: [String: Any] = [
|
|
||||||
"filename": fileName,
|
|
||||||
"content_type": "image/jpeg",
|
|
||||||
"file_size": imageData.count
|
|
||||||
]
|
|
||||||
|
|
||||||
do {
|
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
|
||||||
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB")
|
|
||||||
print("📡 请求参数: \(parameters)")
|
|
||||||
} catch {
|
|
||||||
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 打印调试信息
|
|
||||||
print("🌐 请求URL: \(url.absoluteString)")
|
|
||||||
print("📋 请求头: \(request.allHTTPHeaderFields ?? [:])")
|
|
||||||
|
|
||||||
// 5. 发起获取上传URL的请求
|
|
||||||
print("🌐 正在获取上传链接...")
|
|
||||||
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
|
||||||
// 切换到主线程处理响应
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if let error = error {
|
|
||||||
print("❌ 请求失败: \(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
|
||||||
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"])
|
|
||||||
print("❌ 错误:\(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("📊 响应状态码: \(httpResponse.statusCode)")
|
|
||||||
print("📦 响应头: \(httpResponse.allHeaderFields)")
|
|
||||||
|
|
||||||
guard let data = data else {
|
|
||||||
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "没有接收到数据"])
|
|
||||||
print("❌ 错误:\(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印原始响应数据
|
|
||||||
if let responseString = String(data: data, encoding: .utf8) {
|
|
||||||
print("📡 原始响应数据: \(responseString)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 解析获取上传URL的响应
|
|
||||||
do {
|
|
||||||
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
||||||
throw NSError(domain: "ParseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应数据格式错误"])
|
|
||||||
}
|
|
||||||
|
|
||||||
print("📋 解析后的JSON: \(json)")
|
|
||||||
|
|
||||||
// 根据实际API响应结构调整这里的键名
|
|
||||||
if let uploadUrlString = json["url"] as? String,
|
|
||||||
let uploadUrl = URL(string: uploadUrlString) {
|
|
||||||
print("✅ 成功获取上传链接: \(uploadUrlString)")
|
|
||||||
self?.uploadImageData(imageData, to: uploadUrl, fileName: fileName, completion: completion)
|
|
||||||
} else {
|
|
||||||
throw NSError(domain: "APIError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取上传链接: \(json)"])
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("❌ 解析响应数据失败: \(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uploadImageData(_ data: Data, to url: URL, fileName: String, completion: @escaping (Result<String, Error>) -> Void) {
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.httpMethod = "PUT"
|
|
||||||
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
|
|
||||||
request.httpBody = data
|
|
||||||
|
|
||||||
print("🚀 开始上传图片数据...")
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: request) { _, response, error in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if let error = error {
|
|
||||||
print("❌ 上传失败: \(error.localizedDescription)")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理上传响应
|
|
||||||
if let httpResponse = response as? HTTPURLResponse {
|
|
||||||
print("📊 上传响应状态码: \(httpResponse.statusCode)")
|
|
||||||
|
|
||||||
if (200...299).contains(httpResponse.statusCode) {
|
|
||||||
let fileUrl = "https://your-cdn-domain.com/\(fileName)" // 替换为实际的CDN域名
|
|
||||||
print("✅ 上传成功!文件URL: \(fileUrl)")
|
|
||||||
completion(.success(fileUrl))
|
|
||||||
} else {
|
|
||||||
let errorMessage = "上传失败,状态码: \(httpResponse.statusCode)"
|
|
||||||
print("❌ \(errorMessage)")
|
|
||||||
let error = NSError(domain: "UploadError",
|
|
||||||
code: httpResponse.statusCode,
|
|
||||||
userInfo: [NSLocalizedDescriptionKey: errorMessage])
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let errorMessage = "无效的服务器响应"
|
|
||||||
print("❌ \(errorMessage)")
|
|
||||||
let error = NSError(domain: "UploadError",
|
|
||||||
code: -1,
|
|
||||||
userInfo: [NSLocalizedDescriptionKey: errorMessage])
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 照片选择器,封装了系统相册选择功能
|
/// 照片选择器,封装了系统相册选择功能
|
||||||
|
/// 使用UIViewControllerRepresentable包装PHPickerViewController,提供SwiftUI兼容的图片选择界面
|
||||||
struct PhotoPicker: UIViewControllerRepresentable {
|
struct PhotoPicker: UIViewControllerRepresentable {
|
||||||
// MARK: - 属性
|
// MARK: - Properties
|
||||||
|
|
||||||
/// 绑定的已选图片数组
|
/// 绑定的已选图片数组,用于存储用户选择的图片
|
||||||
@Binding var selectedImages: [UIImage]
|
@Binding var selectedImages: [UIImage]
|
||||||
|
|
||||||
/// 最多可选图片数量
|
/// 最多可选图片数量,默认为1
|
||||||
let selectionLimit: Int
|
let selectionLimit: Int
|
||||||
|
|
||||||
/// 图片过滤器,默认为图片类型
|
/// 图片过滤器,默认为图片类型,可过滤特定类型的媒体
|
||||||
let filter: PHPickerFilter
|
let filter: PHPickerFilter
|
||||||
|
|
||||||
/// 图片上传管理器
|
/// 图片上传完成回调,返回上传结果或错误
|
||||||
private let uploader = ImageUploader()
|
var onImageUploaded: ((Result<ImageUploaderGetID.UploadResult, Error>) -> Void)?
|
||||||
|
|
||||||
// MARK: - 初始化方法
|
// MARK: - Initialization
|
||||||
|
|
||||||
/// 初始化照片选择器
|
/// 初始化照片选择器
|
||||||
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) {
|
/// - Parameters:
|
||||||
|
/// - selectedImages: 绑定的图片数组,用于接收用户选择的图片
|
||||||
|
/// - selectionLimit: 最多可选图片数量,默认为1
|
||||||
|
/// - filter: 媒体类型过滤器,默认为图片
|
||||||
|
/// - onImageUploaded: 图片上传完成后的回调闭包
|
||||||
|
init(
|
||||||
|
selectedImages: Binding<[UIImage]>,
|
||||||
|
selectionLimit: Int = 1,
|
||||||
|
filter: PHPickerFilter = .images,
|
||||||
|
onImageUploaded: ((Result<ImageUploaderGetID.UploadResult, Error>) -> Void)? = nil
|
||||||
|
) {
|
||||||
self._selectedImages = selectedImages
|
self._selectedImages = selectedImages
|
||||||
self.selectionLimit = selectionLimit
|
self.selectionLimit = selectionLimit
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
|
self.onImageUploaded = onImageUploaded
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIViewControllerRepresentable 协议方法
|
// MARK: - UIViewControllerRepresentable
|
||||||
|
|
||||||
|
/// 创建并返回配置好的PHPickerViewController
|
||||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||||
configuration.filter = filter
|
configuration.filter = filter
|
||||||
@ -196,89 +52,131 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
|||||||
return picker
|
return picker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 更新视图控制器(空实现,因为不需要更新)
|
||||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||||
|
|
||||||
|
/// 创建协调器,用于处理PHPickerViewController的代理方法
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(self)
|
Coordinator(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 协调器类
|
// MARK: - Coordinator
|
||||||
|
|
||||||
|
/// 协调器类,处理PHPickerViewController的代理方法
|
||||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||||
|
/// 对父视图的弱引用
|
||||||
let parent: PhotoPicker
|
let parent: PhotoPicker
|
||||||
|
|
||||||
|
/// 图片上传器实例
|
||||||
|
private let uploader = ImageUploaderGetID()
|
||||||
|
|
||||||
init(_ parent: PhotoPicker) {
|
init(_ parent: PhotoPicker) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当用户完成图片选择时调用
|
||||||
|
/// - Parameters:
|
||||||
|
/// - picker: 图片选择器实例
|
||||||
|
/// - results: 用户选择的图片结果数组
|
||||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
|
// 清空已选图片
|
||||||
parent.selectedImages.removeAll()
|
parent.selectedImages.removeAll()
|
||||||
|
|
||||||
|
// 使用DispatchGroup管理多个异步图片加载任务
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
var loadedImages: [Int: UIImage] = [:]
|
var loadedImages: [Int: UIImage] = [:] // 用于保持图片顺序的字典
|
||||||
|
|
||||||
|
// 遍历所有选中的图片
|
||||||
for (index, result) in results.enumerated() {
|
for (index, result) in results.enumerated() {
|
||||||
group.enter()
|
group.enter() // 进入组
|
||||||
|
|
||||||
|
// 检查是否可以加载图片
|
||||||
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||||
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
|
// 异步加载图片
|
||||||
|
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
|
||||||
if let image = image as? UIImage {
|
if let image = image as? UIImage {
|
||||||
// 保存加载的图片,保持原始顺序
|
// 将加载的图片存入字典,保持原始顺序
|
||||||
loadedImages[index] = image
|
loadedImages[index] = image
|
||||||
|
|
||||||
// 上传图片
|
// 上传图片
|
||||||
self.parent.uploader.uploadImage(image) { result in
|
self?.uploader.uploadImage(image) { result in
|
||||||
// 这里可以添加上传完成后的处理
|
// 在主线程中调用上传完成回调
|
||||||
// 目前按需求暂时不处理
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success(let uploadResult):
|
||||||
|
print("✅ 上传成功!fileId: \(uploadResult.fileId)")
|
||||||
|
print("📂 文件信息:")
|
||||||
|
print(" - 文件名: \(uploadResult.fileName)")
|
||||||
|
print(" - 文件大小: \(uploadResult.fileSize) 字节")
|
||||||
|
print(" - 文件URL: \(uploadResult.fileUrl)")
|
||||||
|
|
||||||
|
// 调用上传完成回调
|
||||||
|
self?.parent.onImageUploaded?(.success(uploadResult))
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
print("❌ 上传失败: \(error.localizedDescription)")
|
||||||
|
// 调用上传完成回调,传递错误
|
||||||
|
self?.parent.onImageUploaded?(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
group.leave()
|
group.leave() // 离开组
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
group.leave()
|
group.leave() // 如果无法加载图片,也离开组
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有图片加载完成后的处理
|
||||||
group.notify(queue: .main) {
|
group.notify(queue: .main) {
|
||||||
|
// 按原始顺序排序并更新图片数组
|
||||||
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
|
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
|
||||||
self.parent.selectedImages.append(contentsOf: sortedImages)
|
self.parent.selectedImages.append(contentsOf: sortedImages)
|
||||||
|
|
||||||
|
// 关闭图片选择器
|
||||||
picker.dismiss(animated: true)
|
picker.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 头像上传组件
|
// MARK: - AvatarUploader
|
||||||
|
|
||||||
/// 头像上传视图,提供头像选择功能
|
/// 头像上传视图,提供头像选择功能
|
||||||
|
/// 封装了头像显示和选择逻辑,支持点击选择新头像
|
||||||
struct AvatarUploader: View {
|
struct AvatarUploader: View {
|
||||||
// MARK: - 属性
|
// MARK: - Properties
|
||||||
|
|
||||||
/// 当前选中的头像图片
|
/// 绑定的当前选中头像图片
|
||||||
@Binding var selectedImage: UIImage?
|
@Binding var selectedImage: UIImage?
|
||||||
|
|
||||||
/// 头像尺寸
|
/// 头像显示尺寸
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
// MARK: - 状态
|
/// 上传完成回调,返回上传结果或错误
|
||||||
|
var onUploadComplete: ((Result<ImageUploaderGetID.UploadResult, Error>) -> Void)?
|
||||||
|
|
||||||
/// 是否显示图片选择器
|
// MARK: - State
|
||||||
|
|
||||||
|
/// 控制图片选择器的显示状态
|
||||||
@State private var isImagePickerPresented = false
|
@State private var isImagePickerPresented = false
|
||||||
|
|
||||||
// MARK: - 视图
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: {
|
// 头像按钮,点击后显示图片选择器
|
||||||
isImagePickerPresented = true
|
Button(action: { isImagePickerPresented = true }) {
|
||||||
}) {
|
|
||||||
ZStack {
|
ZStack {
|
||||||
if let selectedImage = selectedImage {
|
if let selectedImage = selectedImage {
|
||||||
|
// 显示已选中的头像
|
||||||
Image(uiImage: selectedImage)
|
Image(uiImage: selectedImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
|
||||||
} else {
|
} else {
|
||||||
|
// 默认头像占位视图
|
||||||
Color.gray.opacity(0.1)
|
Color.gray.opacity(0.1)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -293,10 +191,11 @@ struct AvatarUploader: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle()) // 确保整个区域都可点击
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle()) // 使用无样式按钮
|
||||||
.sheet(isPresented: $isImagePickerPresented) {
|
.sheet(isPresented: $isImagePickerPresented) {
|
||||||
|
// 显示图片选择器
|
||||||
PhotoPicker(
|
PhotoPicker(
|
||||||
selectedImages: Binding(
|
selectedImages: Binding(
|
||||||
get: { [selectedImage].compactMap { $0 } },
|
get: { [selectedImage].compactMap { $0 } },
|
||||||
@ -304,7 +203,11 @@ struct AvatarUploader: View {
|
|||||||
selectedImage = images.first
|
selectedImage = images.first
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
selectionLimit: 1
|
selectionLimit: 1, // 限制只能选择一张图片
|
||||||
|
onImageUploaded: { result in
|
||||||
|
// 图片上传完成后的处理
|
||||||
|
onUploadComplete?(result)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
wake/View/Components/Upload/Compression.swift
Normal file
0
wake/View/Components/Upload/Compression.swift
Normal file
232
wake/View/Components/Upload/ImageUploaderGetID.swift
Normal file
232
wake/View/Components/Upload/ImageUploaderGetID.swift
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
|
/// 处理图片上传到远程服务器的类
|
||||||
|
/// 支持上传图片并获取服务器返回的file_id
|
||||||
|
public class ImageUploaderGetID: ObservableObject {
|
||||||
|
// MARK: - 类型定义
|
||||||
|
|
||||||
|
/// 上传结果
|
||||||
|
public struct UploadResult {
|
||||||
|
public let fileUrl: String
|
||||||
|
public let fileName: String
|
||||||
|
public let fileSize: Int
|
||||||
|
public let fileId: String
|
||||||
|
|
||||||
|
public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String) {
|
||||||
|
self.fileUrl = fileUrl
|
||||||
|
self.fileName = fileName
|
||||||
|
self.fileSize = fileSize
|
||||||
|
self.fileId = fileId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上传过程中可能发生的错误
|
||||||
|
public enum UploadError: LocalizedError {
|
||||||
|
case invalidImageData
|
||||||
|
case invalidURL
|
||||||
|
case serverError(String)
|
||||||
|
case invalidResponse
|
||||||
|
case uploadFailed(Error?)
|
||||||
|
case invalidFileId
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .invalidImageData:
|
||||||
|
return "无效的图片数据"
|
||||||
|
case .invalidURL:
|
||||||
|
return "无效的URL"
|
||||||
|
case .serverError(let message):
|
||||||
|
return "服务器错误: \(message)"
|
||||||
|
case .invalidResponse:
|
||||||
|
return "无效的服务器响应"
|
||||||
|
case .uploadFailed(let error):
|
||||||
|
return "上传失败: \(error?.localizedDescription ?? "未知错误")"
|
||||||
|
case .invalidFileId:
|
||||||
|
return "无效的文件ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 属性
|
||||||
|
|
||||||
|
private let session: URLSession
|
||||||
|
private let apiConfig: APIConfig.Type
|
||||||
|
|
||||||
|
// MARK: - 初始化方法
|
||||||
|
|
||||||
|
/// 初始化方法
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: 可选的URLSession,用于测试依赖注入
|
||||||
|
/// - apiConfig: 可选的API配置,用于测试依赖注入
|
||||||
|
public init(session: URLSession = .shared, apiConfig: APIConfig.Type = APIConfig.self) {
|
||||||
|
self.session = session
|
||||||
|
self.apiConfig = apiConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 公开方法
|
||||||
|
|
||||||
|
/// 上传图片到服务器
|
||||||
|
/// - Parameters:
|
||||||
|
/// - image: 要上传的图片
|
||||||
|
/// - completion: 完成回调,返回Result类型的结果
|
||||||
|
public func uploadImage(_ image: UIImage, completion: @escaping (Result<UploadResult, Error>) -> Void) {
|
||||||
|
print("🔄 开始准备上传图片...")
|
||||||
|
|
||||||
|
// 1. 转换图片为Data
|
||||||
|
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
|
||||||
|
let error = UploadError.invalidImageData
|
||||||
|
print("❌ 错误:\(error.localizedDescription)")
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取上传URL
|
||||||
|
getUploadURL(for: imageData) { [weak self] result in
|
||||||
|
switch result {
|
||||||
|
case .success((let fileId, let uploadURL)):
|
||||||
|
// 3. 确认上传
|
||||||
|
self?.confirmUpload(fileId: fileId, fileName: "avatar_\(UUID().uuidString).jpg", fileSize: imageData.count) { confirmResult in
|
||||||
|
completion(confirmResult)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 私有方法
|
||||||
|
|
||||||
|
/// 获取上传URL
|
||||||
|
private func getUploadURL(for imageData: Data, completion: @escaping (Result<(fileId: String, uploadURL: URL), Error>) -> Void) {
|
||||||
|
let fileName = "avatar_\(UUID().uuidString).jpg"
|
||||||
|
let parameters: [String: Any] = [
|
||||||
|
"filename": fileName,
|
||||||
|
"content_type": "image/jpeg",
|
||||||
|
"file_size": imageData.count
|
||||||
|
]
|
||||||
|
|
||||||
|
let urlString = "\(apiConfig.baseURL)/file/generate-upload-url"
|
||||||
|
guard let url = URL(string: urlString) else {
|
||||||
|
completion(.failure(UploadError.invalidURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.allHTTPHeaderFields = apiConfig.authHeaders
|
||||||
|
|
||||||
|
do {
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
||||||
|
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB")
|
||||||
|
} catch {
|
||||||
|
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = session.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(UploadError.uploadFailed(error)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
completion(.failure(UploadError.invalidResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = data else {
|
||||||
|
completion(.failure(UploadError.invalidResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印调试信息
|
||||||
|
if let responseString = String(data: data, encoding: .utf8) {
|
||||||
|
print("📥 获取上传URL响应: \(responseString)")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
let dataDict = json["data"] as? [String: Any],
|
||||||
|
let fileId = dataDict["file_id"] as? String,
|
||||||
|
let uploadURLString = dataDict["upload_url"] as? String,
|
||||||
|
let uploadURL = URL(string: uploadURLString) else {
|
||||||
|
throw UploadError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(.success((fileId: fileId, uploadURL: uploadURL)))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(UploadError.invalidResponse))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 确认上传
|
||||||
|
private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result<UploadResult, Error>) -> Void) {
|
||||||
|
let urlString = "\(apiConfig.baseURL)/file/confirm-upload"
|
||||||
|
guard let url = URL(string: urlString) else {
|
||||||
|
completion(.failure(UploadError.invalidURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.allHTTPHeaderFields = apiConfig.authHeaders
|
||||||
|
|
||||||
|
let requestBody: [String: Any] = ["file_id": fileId]
|
||||||
|
|
||||||
|
do {
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = session.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(UploadError.uploadFailed(error)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
completion(.failure(UploadError.serverError("确认上传失败,状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建上传结果
|
||||||
|
let uploadResult = UploadResult(
|
||||||
|
fileUrl: "\(self.apiConfig.baseURL)/files/\(fileId)",
|
||||||
|
fileName: fileName,
|
||||||
|
fileSize: fileSize,
|
||||||
|
fileId: fileId
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ 图片上传并确认成功,fileId: \(fileId)")
|
||||||
|
completion(.success(uploadResult))
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 响应模型
|
||||||
|
|
||||||
|
struct UploadURLResponse: Codable {
|
||||||
|
let code: Int
|
||||||
|
let message: String
|
||||||
|
let data: UploadData
|
||||||
|
|
||||||
|
struct UploadData: Codable {
|
||||||
|
let fileId: String
|
||||||
|
let uploadUrl: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case fileId = "file_id"
|
||||||
|
case uploadUrl = "upload_url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user