You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift客户端向Vapor服务器同时发送图片与JSON的实现咨询

问题解答

一、multipart/form-data是否适合你的场景?

是的,这是最优方案。因为你需要同时传输结构化的用户注册数据(文本、布尔值等)和二进制的头像图片,multipart/form-data就是为这种混合内容传输设计的标准格式,能避免数据编码冲突,且被绝大多数后端框架支持。


二、Swift端扩展MRNetwork库支持multipart请求

MRNetwork基于URLSession封装,我们可以扩展它的请求构建能力,添加multipart表单的支持。以下是具体实现:

1. 扩展MRNetwork添加multipart发送方法

import Foundation

extension MRNetwork {
    /// 发送包含图片和表单数据的multipart请求
    func sendMultipartRequest<T: Codable>(
        url: String,
        method: HTTPMethod,
        formParameters: [String: Any],
        imageData: Data,
        imageFieldName: String = "profileImage",
        fileName: String = "avatar.jpg",
        mimeType: String = "image/jpeg",
        responseType: T.Type,
        completion: @escaping (Result<T, Error>) -> Void
    ) {
        guard let url = URL(string: url) else {
            completion(.failure(NSError(domain: "MRNetwork", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
            return
        }
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = method.rawValue
        
        // 生成multipart边界标识
        let boundary = UUID().uuidString
        urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
        // 构建请求体
        var body = Data()
        
        // 添加表单参数
        for (key, value) in formParameters {
            body.append("--\(boundary)\r\n".data(using: .utf8)!)
            body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
            body.append("\(value)\r\n".data(using: .utf8)!)
        }
        
        // 添加图片文件
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"\(imageFieldName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
        
        urlRequest.httpBody = body
        
        // 利用MRNetwork现有逻辑发送请求
        sendRequest(request: urlRequest, responseType: responseType, completion: completion)
    }
}

2. SwiftUI中调用示例

// 假设你已获取用户选择的头像UIImage
guard let avatarData = avatarImage.jpegData(compressionQuality: 0.8) else {
    print("头像转Data失败")
    return
}

// 构建注册表单参数
let formParams: [String: Any] = [
    "name": "张三",
    "username": "zhangsan",
    "email": "zhangsan@example.com",
    "password": "123456",
    "repeatPassword": "123456",
    "APIKey": "你的APIKeyBase64字符串",
    "termsConditions": true,
    "rating": 0.0
]

// 发送注册请求
MRNetwork.shared.sendMultipartRequest(
    url: "http://你的服务器地址/api/users",
    method: .post,
    formParameters: formParams,
    imageData: avatarData,
    responseType: HTTPStatus.self,
    completion: { result in
        switch result {
        case .success(let status):
            print("注册成功:\(status)")
        case .failure(let error):
            print("注册失败:\(error.localizedDescription)")
        }
    }
)

三、Vapor服务器端接收并处理multipart数据

你需要修改原有的create函数,从multipart表单中解析字段和图片,再完成用户创建与图片存储。以下是修改后的代码:

1. 修改后的create函数

func create(req: Request) async throws -> HTTPStatus {
    // 解析multipart表单
    let form = try await req.content.decode(MultipartForm.self)
    
    // 提取必填字段并做类型校验
    guard let name = form["name"]?.string,
          let username = form["username"]?.string,
          let email = form["email"]?.string,
          let password = form["password"]?.string,
          let repeatPassword = form["repeatPassword"]?.string,
          let apiKeyStr = form["APIKey"]?.string,
          let termsAccepted = form["termsConditions"]?.bool,
          let rating = form["rating"]?.double else {
        throw Abort(.badRequest, reason: "缺少必填字段或字段格式错误")
    }
    
    // APIKey验证逻辑(保持原有逻辑)
    guard let apiKeyData = Data(base64Encoded: apiKeyStr),
          apiKeyData == Data(AK) else {
        throw Abort(.badRequest, reason: "无效的请求权限")
    }
    
    // 密码匹配验证(保持原有逻辑)
    guard password == repeatPassword else {
        throw Abort(.badRequest, reason: "两次输入的密码不一致")
    }
    
    // 检查用户名是否已存在(保持原有逻辑)
    let existingUser = try await Users.query(on: req.db)
        .filter(\.$username == username)
        .first()
    
    guard existingUser == nil else {
        throw Abort(.badRequest, reason: "用户名已被占用")
    }
    
    // 创建并保存用户
    let newUser = Users(
        name: name,
        username: username,
        email: email,
        password: try Bcrypt.hash("\(username):\(password)"),
        termsConditions: termsAccepted
    )
    try await newUser.save(on: req.db)
    
    // 处理头像图片
    let profileImagePath: String
    if let imagePart = form["profileImage"]?.file {
        // 生成唯一文件名避免冲突
        let fileName = "\(UUID().uuidString).jpg"
        // 图片存储路径:服务器Public/avatars目录
        let saveDir = req.application.directory.publicDirectory.appendingPathComponent("avatars")
        let filePath = saveDir.appendingPathComponent(fileName)
        
        // 创建目录(如果不存在)
        try FileManager.default.createDirectory(at: saveDir, withIntermediateDirectories: true)
        // 写入图片数据
        try await imagePart.data.write(to: filePath)
        
        // 存储相对路径(供前端访问)
        profileImagePath = "/avatars/\(fileName)"
    } else {
        // 无头像时使用默认图路径
        profileImagePath = "/avatars/default.jpg"
    }
    
    // 创建用户Profile
    let newProfile = Profiles(
        profileImage: profileImagePath,
        averageRating: rating,
        user: try newUser.requireID()
    )
    try await newUser.$profile.create(newProfile, on: req.db)
    
    return .created
}

2. 服务器配置补充

在configure.swift中添加静态文件中间件,允许前端访问存储的头像:

// 允许访问Public目录下的静态文件
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))

内容的提问来源于stack exchange,提问作者Miguelon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 17:02:07