修复素材上传 OSS 路径并完善调试与表单交互。
对齐 Android material/{scenicId}/0/ 目录结构以修复视频元数据写入失败,补充 API 请求体调试日志、素材拍摄麦克风权限与上传控件点击穿透修复。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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;
|
||||
|
||||
@ -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 ?? "<invalid url>"
|
||||
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 "<multipart/form-data: \(data.count) bytes>"
|
||||
}
|
||||
|
||||
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 "<non-utf8 body: \(data.count) bytes>"
|
||||
}
|
||||
|
||||
/// 将响应体格式化为便于调试阅读的字符串。
|
||||
private static func debugResponseBody(from data: Data) -> String {
|
||||
guard !data.isEmpty else { return "<empty response>" }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user