From 7deeedec6828686030ede38b0a3d6b54e71563a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Mon, 13 Jul 2026 11:19:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=B4=A0=E6=9D=90=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=20OSS=20=E8=B7=AF=E5=BE=84=E5=B9=B6=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E4=B8=8E=E8=A1=A8=E5=8D=95=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对齐 Android material/{scenicId}/0/ 目录结构以修复视频元数据写入失败,补充 API 请求体调试日志、素材拍摄麦克风权限与上传控件点击穿透修复。 Co-authored-by: Cursor --- suixinkan.xcodeproj/project.pbxproj | 2 + suixinkan/Core/Networking/APIClient.swift | 44 ++++++++++++++++++- suixinkan/Core/Upload/OSSUploadService.swift | 3 +- .../MaterialFormViewController.swift | 8 ++++ .../MaterialManagementAPITests.swift | 18 ++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/suixinkan.xcodeproj/project.pbxproj b/suixinkan.xcodeproj/project.pbxproj index faefe8e..7842a87 100644 --- a/suixinkan.xcodeproj/project.pbxproj +++ b/suixinkan.xcodeproj/project.pbxproj @@ -414,6 +414,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "随心瞰商家版"; INFOPLIST_KEY_NSCameraUsageDescription = "需要访问相机以拍摄任务素材、扫描订单核销二维码,并连接有线相机进行旅拍相册传输"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要获取您的位置信息,用于位置上报、景区距离排序及多点位核销打卡"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "需要访问麦克风以在拍摄素材视频时录制声音"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "需要保存收款二维码、排队打卡点小程序码和云盘图片视频到相册"; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "需要访问相册以选择头像、身份证、银行卡照片和云盘上传素材"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -478,6 +479,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "随心瞰商家版"; INFOPLIST_KEY_NSCameraUsageDescription = "需要访问相机以拍摄任务素材、扫描订单核销二维码,并连接有线相机进行旅拍相册传输"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要获取您的位置信息,用于位置上报、景区距离排序及多点位核销打卡"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "需要访问麦克风以在拍摄素材视频时录制声音"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "需要保存收款二维码、排队打卡点小程序码和云盘图片视频到相册"; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "需要访问相册以选择头像、身份证、银行卡照片和云盘上传素材"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/suixinkan/Core/Networking/APIClient.swift b/suixinkan/Core/Networking/APIClient.swift index 1fa3bad..1693a86 100644 --- a/suixinkan/Core/Networking/APIClient.swift +++ b/suixinkan/Core/Networking/APIClient.swift @@ -340,12 +340,30 @@ final class APIClient { } } - /// 在 Debug 环境打印请求信息。 + /// 在 Debug 环境打印请求信息(含 GET 查询参数与 POST 请求体,对齐 Android Ktor `LogLevel.BODY`)。 private func logRequest(_ request: URLRequest) { #if DEBUG let method = request.httpMethod ?? "REQUEST" let url = request.url?.absoluteString ?? "" - print("[API][Request] \(method) \(url)") + var lines = ["[API][Request] \(method) \(url)"] + + if let queryItems = URLComponents(url: request.url ?? URL(fileURLWithPath: "/"), resolvingAgainstBaseURL: false)?.queryItems, + !queryItems.isEmpty { + let params = queryItems + .map { item in + let value = item.value ?? "" + return "\(item.name)=\(value)" + } + .joined(separator: "&") + lines.append("params: \(params)") + } + + let contentType = request.value(forHTTPHeaderField: "Content-Type") + if let body = Self.debugRequestBody(from: request.httpBody, contentType: contentType) { + lines.append("body:\n\(body)") + } + + print(lines.joined(separator: "\n")) #endif } @@ -370,6 +388,28 @@ final class APIClient { } #if DEBUG + /// 将请求体格式化为便于调试阅读的字符串。 + private static func debugRequestBody(from data: Data?, contentType: String?) -> String? { + guard let data, !data.isEmpty else { return nil } + + if let contentType, contentType.lowercased().contains("multipart/form-data") { + return "" + } + + if + let object = try? JSONSerialization.jsonObject(with: data), + let prettyData = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]), + let prettyJSON = String(data: prettyData, encoding: .utf8) { + return prettyJSON + } + + if let text = String(data: data, encoding: .utf8), !text.isEmpty { + return text.count > 2_000 ? String(text.prefix(2_000)) + "..." : text + } + + return "" + } + /// 将响应体格式化为便于调试阅读的字符串。 private static func debugResponseBody(from data: Data) -> String { guard !data.isEmpty else { return "" } diff --git a/suixinkan/Core/Upload/OSSUploadService.swift b/suixinkan/Core/Upload/OSSUploadService.swift index ea283b5..14bbd64 100644 --- a/suixinkan/Core/Upload/OSSUploadService.swift +++ b/suixinkan/Core/Upload/OSSUploadService.swift @@ -314,7 +314,8 @@ enum OSSUploadPolicy { case "sample": return "sample/\(currentDate)/\(scenicId)/\(uploadId)_\(safeName)" case "material": - return "material/\(currentDate)/\(scenicId)/\(uploadId)_\(safeName)" + // Android 素材任务固定 folderId = 0,后端会按此层级解析对象信息并写入视频元数据。 + return "material/\(scenicId)/0/\(uploadId)_\(safeName)" case "punch_point": let userId = AppStore.shared.session.userId.trimmingCharacters(in: .whitespacesAndNewlines) let ownerId = userId.isEmpty ? String(scenicId) : userId diff --git a/suixinkan/UI/MaterialManagement/MaterialFormViewController.swift b/suixinkan/UI/MaterialManagement/MaterialFormViewController.swift index 45c53d1..cd6e704 100644 --- a/suixinkan/UI/MaterialManagement/MaterialFormViewController.swift +++ b/suixinkan/UI/MaterialManagement/MaterialFormViewController.swift @@ -685,9 +685,12 @@ private final class MaterialUploadThumbView: UIControl { addCircle.backgroundColor = AppColor.primary addCircle.layer.cornerRadius = 14 addIcon.tintColor = .white + addCircle.isUserInteractionEnabled = false + addIcon.isUserInteractionEnabled = false textLabel.font = .systemFont(ofSize: 12) textLabel.textColor = UIColor(hex: 0x4B5563) textLabel.textAlignment = .center + textLabel.isUserInteractionEnabled = false deleteButton.setImage(UIImage(named: "material_ic_clear"), for: .normal) deleteButton.tintColor = .white deleteButton.backgroundColor = UIColor.black.withAlphaComponent(0.6) @@ -835,17 +838,22 @@ private final class MaterialCoverUploadView: UIControl { clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true + imageView.isUserInteractionEnabled = false addCircle.backgroundColor = AppColor.primary addCircle.layer.cornerRadius = 16 addIcon.tintColor = .white + addCircle.isUserInteractionEnabled = false + addIcon.isUserInteractionEnabled = false titleLabel.text = "点击上传图片" titleLabel.font = .systemFont(ofSize: 14) titleLabel.textColor = UIColor(hex: 0x4B5563) titleLabel.textAlignment = .center + titleLabel.isUserInteractionEnabled = false hintLabel.text = "建议尺寸: 1920x1080px, 大小不超过5MB" hintLabel.font = .systemFont(ofSize: 12) hintLabel.textColor = UIColor(hex: 0xB6BECA) hintLabel.textAlignment = .center + hintLabel.isUserInteractionEnabled = false deleteButton.setImage(UIImage(named: "material_ic_clear"), for: .normal) deleteButton.tintColor = .white deleteButton.backgroundColor = UIColor.black.withAlphaComponent(0.6) diff --git a/suixinkanTests/MaterialManagementAPITests.swift b/suixinkanTests/MaterialManagementAPITests.swift index 7366ef3..4769444 100644 --- a/suixinkanTests/MaterialManagementAPITests.swift +++ b/suixinkanTests/MaterialManagementAPITests.swift @@ -114,6 +114,24 @@ final class MaterialManagementAPITests: XCTestCase { XCTAssertNil(editBody["project_id"]) } + func testMaterialOSSObjectKeyMatchesAndroidFolderLayout() { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + let date = calendar.date(from: DateComponents(year: 2026, month: 7, day: 13))! + let uuid = UUID(uuidString: "11111111-2222-3333-4444-555555555555")! + + let key = OSSUploadPolicy.objectKey( + fileName: "video/demo.mp4", + scenicId: 9, + moduleType: "material", + date: date, + uuid: uuid, + timeZone: TimeZone(secondsFromGMT: 0)! + ) + + XCTAssertEqual(key, "material/9/0/11111111222233334444555555555555_video_demo.mp4") + } + private func envelopeJSON(_ dataJSON: String) -> Data { """ {"code":100000,"msg":"success","data":\(dataJSON)}