From 54aeb47fd969681057f66e21ad3aae18be20c229 Mon Sep 17 00:00:00 2001 From: zeng Date: Tue, 27 Jan 2026 15:16:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8A=E4=BC=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InAppPurchseView/APUploadIAPListVC.swift | 234 +++++++++--------- .../Shared/Network/AppStoreConnectAPI.swift | 179 +++++++------- 2 files changed, 217 insertions(+), 196 deletions(-) diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift index 20d24e7..5524cef 100644 --- a/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift @@ -8,12 +8,12 @@ import Cocoa class APUploadIAPListVC: NSViewController { - + @IBOutlet weak var tableView: NSTableView! @IBOutlet weak var enterBtn: NSButton! @IBOutlet weak var preserveCurrentPriceBtn: NSButton! @IBOutlet weak var showApiRateLimitLogsBtn: NSButton! - + public var currentApp: App? { didSet { setupUI() @@ -24,22 +24,22 @@ class APUploadIAPListVC: NSViewController { self.tableView.reloadData() } } - + private var screenshotPaths = [String: String]() - + override func viewDidLoad() { super.viewDidLoad() - + self.tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle self.tableView.selectionHighlightStyle = .none self.tableView.sizeToFit() } - + func setupUI() { self.view.window?.title = "批量内购买项目上传 - " + (currentApp?.appName ?? "") self.tableView.reloadData() } - + func showUploadView() { // 不能同时 present 出来 DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { @@ -58,36 +58,36 @@ class APUploadIAPListVC: NSViewController { self.presentAsSheet(upVC!) } } - + @IBAction func clickedUploadShotBtn(_ sender: Any) { showUploadView() } - + @IBAction func clickedSPasswordBtn(_ sender: Any) { let vc = APASCKeysSettingVC() presentAsSheet(vc) } - + @IBAction func createIAP(_ sender: Any) { let list = self.iaps guard list.count > 0 else { APHUD.hide(message: "当前 App 无上传的内购商品!", delayTime: 1) return } - - + + guard let appid = currentApp?.appId else { APHUD.hide(message: "当前 App 的 appleid 为空!", delayTime: 1) return } - + enterBtn.isEnabled = false APHUD.show(message: "上传中", view: self.view) let uploadIAPs: ((AppStoreConnectKey) -> Void) = { [weak self] ascKey in // 上传数据 self?.updateInAppPurchse(iaps: list, appId: appid, ascKey: ascKey) } - + guard let ascKey = InfoCenter.shared.currentASCKey else { let vc = APASCKeysSettingVC() vc.updateCompletion = { password in @@ -98,7 +98,7 @@ class APUploadIAPListVC: NSViewController { presentAsSheet(vc) return } - + uploadIAPs(ascKey) } @@ -106,7 +106,7 @@ class APUploadIAPListVC: NSViewController { // MARK: - 网络请求 extension APUploadIAPListVC { - + func updateInAppPurchse(iaps: [IAPProduct], appId: String, ascKey: AppStoreConnectKey) { let showApiRateLimit = showApiRateLimitLogsBtn.state.rawValue == 1 let ascAPI = APASCAPI.init(issuerID: ascKey.issuerID, @@ -114,7 +114,7 @@ extension APUploadIAPListVC { privateKey: ascKey.privateKey, showApiRateLimit: showApiRateLimit) ascAPI.addMessage("密钥信息:\(ascKey.issuerID), \(ascKey.privateKeyID), \(ascKey.privateKey)") - + Task { // 1、获取当前账号下 app,判断是否包含当前 提交商品的 app guard let apps = await ascAPI.apps() else { @@ -130,7 +130,7 @@ extension APUploadIAPListVC { APHUD.hide(message: "当前的密钥没有查到App: \(appId),请检查~", delayTime: 2) return } - + // 2. 同步显示进度日志 let sb = NSStoryboard(name: "APDebugVC", bundle: Bundle(for: self.classForCoder)) let newWC = sb.instantiateController(withIdentifier: "APDebugWC") as? NSWindowController @@ -141,11 +141,11 @@ extension APUploadIAPListVC { ascAPI.updateMsg = { messages in logVC?.debugLog = messages.joined(separator: "\n") } - + ascAPI.addMessage("开始处理内购商品,获取现有商品中...") // 3、获取所有的内购商品,如果存在的商品就直接修改,如果不存在就创建 let oldIAPs = await ascAPI.fetchInAppPurchasesList(appId: appId) - + // 4、遍历所有要上传的商品 for product in iaps { // 订阅类型与非订单类型不一样的处理逻辑 @@ -155,15 +155,15 @@ extension APUploadIAPListVC { await createInAppPurchase(appId: appId, product: product, oldIAPs: oldIAPs, ascAPI: ascAPI) } } - + self.enterBtn.isEnabled = true APHUD.hide() ascAPI.addMessage("完成全部内购商品,可稍后在苹果后台查看!✅✅✅") } } - + // MARK: - 上传内购类型商品 - + /// 创建内购商品 func createInAppPurchase(appId: String, product: IAPProduct, oldIAPs: [ASCInAppPurchaseV2], ascAPI: APASCAPI) async { ascAPI.addMessage("开始上传内购商品:\(product.productId),\(product.name) ") @@ -172,7 +172,7 @@ extension APUploadIAPListVC { if let iap = iaps.first { ascAPI.addMessage("订阅商品已经存在:\(product.productId) ,跳过更新信息...") return; - + ascAPI.addMessage("内购已经存在:\(product.productId) ,开始更新信息中...") // 0. 审核备注如果原来有值,而新字段无值,则使用原值 var product = product @@ -185,8 +185,8 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购已经存在:\(product.productId) ,更新信息失败!❌ ") return } - - + + // 3. 商品本地化语言 ascAPI.addMessage("开始更新内购本地化版本:\(product.productId)") let localizations = await ascAPI.fetchInAppPurchasesLocalizations(iapId: iap.id) @@ -208,16 +208,16 @@ extension APUploadIAPListVC { await createIAPLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) } } - + // 4. 商品截图 await createIAPScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 5. 销售国家或地区 await updateIAPAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 2. 商品价格档位 await updateIAPPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) - + } else { // 1. 创建新的商品 guard let iap = await ascAPI.createInAppPurchases(appId: appId, product: product) else { @@ -225,48 +225,48 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品:\(product.productId) ,创建失败!❌ ") return } - - + + // 3. 商品本地化语言 for localization in product.localizations { await createIAPLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) } - + // 4. 商品截图 await createIAPScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 5. 销售国家或地区 await updateIAPAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 2. 商品价格档位 await updateIAPPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) } - + ascAPI.addMessage("内购商品:\(product.productId),\(product.name) ,上传完成!\n") } - + /// 创建内购商品价格档位 func updateIAPPricePoint(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { guard let schedule = product.priceSchedules else { ascAPI.addMessage("无价格计划表:\(product.productId) ,请确认!❌ ") return } - + let baseTerritory = schedule.baseTerritory let baseCustomerPrice = schedule.baseCustomerPrice.normalizePrice() - + ascAPI.addMessage("开始更新价格计划表:\(product.productId),\(baseTerritory),\(baseCustomerPrice) \n") - + let points = await ascAPI.fetchPricePoints(iapId: iapId, territory: [baseTerritory]) if let point = points.filter({ $0.attributes?.customerPrice!.normalizePrice() == baseCustomerPrice }).first { var manualPrices: [Any] = [] var included: [Any] = [] - + ascAPI.addMessage("开始构建基准国家和自定价格:") // base Territory manualPrices.append(["id": "${\(baseTerritory)-\(included.count)}", "type": "inAppPurchasePrices"]) included.append(ascAPI.fetchInAppPurchasePriceSchedule(scheduleId: baseTerritory, pricePointId: point.id, iapId: iapId, index: included.count)) - + // customerPrice for pricePoint in schedule.manualPrices { let territory = pricePoint.territory @@ -279,9 +279,9 @@ extension APUploadIAPListVC { ascAPI.addMessage("自定价格的内购价格点:\(territory),\(customerPrice) ,未找到此档位!❌ ") } } - + ascAPI.saveLogs(log: "内购的基准国家和自定价格:\(manualPrices),\(included)") - + if (await ascAPI.updateInAppPurchasePricePoint(iapId: iapId, baseTerritoryId: baseTerritory, manualPrices: manualPrices, included: included)) != nil { // 价格档位配置成功 ascAPI.addMessage("内购价格点:\(baseTerritory),\(baseCustomerPrice) ,更新价格成功!✅ ") @@ -294,8 +294,8 @@ extension APUploadIAPListVC { ascAPI.addMessage("基准国家的内购价格点:\(baseTerritory),\(baseCustomerPrice) ,未找到此档位!❌ ") } } - - + + /// 创建内购商品本地化信息 func createIAPLocalization(iapId: String, localization: IAPLocalization, product: IAPProduct, ascAPI: APASCAPI) async { ascAPI.addMessage("开始更新本地化版本:\(product.productId),\(localization.locale)") @@ -307,7 +307,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购本地化版本:\(localization.locale) ,更新语言失败!❌ ") } } - + /// 更新内购商品的送审截图 func createIAPScreenshot(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { ascAPI.addMessage("开始更新内购商品的送审截图:\(product.productId),\(product.reviewScreenshot)") @@ -316,8 +316,9 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品:\(product.productId) 无送审截图或未上传截图~") return } - + let imaUrl = URL.init(fileURLWithPath: imgPath) + let uploadFileName = imaUrl.lastPathComponent.isEmpty ? imgName : imaUrl.lastPathComponent guard let fileMD5 = URL.init(fileURLWithPath: imgPath).fileMD5() else { ascAPI.addMessage("内购商品截图文件错误:\(imgPath) ,无法生成 md5 值~") return @@ -332,16 +333,20 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品截图创建失败:\(imgName) ,无法删除旧截图~") } } - - ascAPI.addMessage("创建新的送审截图:\(product.reviewScreenshot)") + + ascAPI.addMessage("创建新的送审截图:\(uploadFileName)") // 创建截图 let imaSize = imaUrl.fileSizeInt() - guard let shot = await ascAPI.createInAppPurchasesScreenshot(iapId: iapId, fileName: imgName, fileSize: imaSize) else { + guard imaSize > 0 else { + ascAPI.addMessage("内购商品截图文件错误:\(imgPath) ,文件大小为 0~") + return + } + guard let shot = await ascAPI.createInAppPurchasesScreenshot(iapId: iapId, fileName: uploadFileName, fileSize: imaSize) else { // 创建失败 ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!❌ ") return } - + // 根据苹果接口返回的上传接口上传 guard let method = shot.attributes?.uploadOperations?.first?.method, let url = shot.attributes?.uploadOperations?.first?.url, @@ -350,14 +355,14 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!苹果参数异常~ ❌ ") return } - + var request = URLRequest(url: baseURL) request.httpMethod = method for header in requestHeaders { request.headers[header.name ?? ""] = header.value ?? "" } - - ascAPI.addMessage("上传新的送审截图:\(product.reviewScreenshot)") + + ascAPI.addMessage("上传新的送审截图:\(uploadFileName)") // 上传图片 guard let response = try? await URLSession.shared.upload(for: request, fromFile: imaUrl) else { ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!上传图片异常~ ❌ ") @@ -367,8 +372,8 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!上传图片异常 \(response.1.description)~ ❌ ") return } - - ascAPI.addMessage("提交新的送审截图:\(product.reviewScreenshot)") + + ascAPI.addMessage("提交新的送审截图:\(uploadFileName)") // 确认图片 if ((await ascAPI.updateInAppPurchasesScreenshot(iapShotId: shot.id, fileMD5: fileMD5)) != nil) { ascAPI.addMessage("内购商品:\(product.productId) ,送审截图上传成功!✅ ") @@ -376,7 +381,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品:\(product.productId) ,送审截图可能上传失败! ") } } - + /// 销售国家或地区 func updateIAPAvailableTerritories(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { let inAll = product.territories.availableInAllTerritories @@ -384,7 +389,7 @@ extension APUploadIAPListVC { let summary = territoryInfo(product: product) let newTerritory = inNew ? "将来新国家(地区)时自动提供!" : "将来新国家(地区)时不自动提供!" ascAPI.addMessage("开始更新内购商品的销售国家/地区:\(summary)") - + guard !inAll else { var allTerritories: [[String: String]] = [] if let territories = await ascAPI.territories() { @@ -405,7 +410,7 @@ extension APUploadIAPListVC { } return } - + /// 选择销售的国家或地区 var territories: [[String: String]] = [] product.territories.territories?.forEach({ territory in @@ -414,7 +419,7 @@ extension APUploadIAPListVC { "id": territory.id ]) }) - + let customerTerritory = product.territories.territories?.map({ $0.id }).joined(separator: ",") ?? "无" if (await ascAPI.updateInAppPurchasesAvailabilityTerritories(iapId: iapId, availableTerritories: territories, availableInNewTerritories: inNew)) != nil { ascAPI.addMessage("内购商品的销售国家/地区:\(customerTerritory) ,更新成功!✅ ") @@ -422,10 +427,10 @@ extension APUploadIAPListVC { ascAPI.addMessage("内购商品的销售国家/地区:\(customerTerritory) ,更新失败!❌ ") } } - - + + // MARK: - 上传订阅商品 - + /// 订阅商品创建或更新 func createRenewSubscription(appId: String, product: IAPProduct, ascAPI: APASCAPI) async { let groupName = product.groupName @@ -434,46 +439,46 @@ extension APUploadIAPListVC { var subGroups = await ascAPI.fetchSubscriptionGroups(appId: appId) // if subGroups.isEmpty { // } - + for subGroup in subGroups { if subGroup.attributes?.referenceName == groupName { currentSubGroup = subGroup } } - + // 创建订阅组 if currentSubGroup == nil, let group = await ascAPI.createSubscriptionGroups(appId: appId, groupName: groupName) { currentSubGroup = group subGroups.append(group) - + for localization in product.localizations { let _ = await ascAPI.createSubscriptionGroupLocalizations(iapGroupId: group.id, name: localization.name, locale: localization.locale, customAppName: nil) } } - + //订阅组设置国际化 if let group = currentSubGroup { if let localization = product.groupLocalization { let _ = await ascAPI.createSubscriptionGroupLocalizations(iapGroupId: group.id, name: localization.name, locale: localization.locale, customAppName: nil) } } - + // 2、有订阅组,获取所有订阅组的所有订阅商品 var subscriptions = [ASCSubscription]() for subGroup in subGroups { let subs = await ascAPI.fetchSubscriptionGroupSubscriptions(iapGroupId: subGroup.id) subscriptions.append(contentsOf: subs) } - - - + + + // 3、查看是否存在订阅商品,不存在就创建,存在就更新 let subs = subscriptions.filter({ $0.attributes?.productID == product.productId }) if let sub = subs.first { ///不做更新 ascAPI.addMessage("订阅商品已经存在:\(product.productId) ,跳过更新信息...") return; - + ascAPI.addMessage("订阅商品已经存在:\(product.productId) ,开始更新信息中...") // 0. 审核备注如果原来有值,而新字段无值,则使用原值 var product = product @@ -486,7 +491,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品已经存在:\(product.productId) ,更新信息失败!❌ ") return } - + // 2. 商品本地化语言 ascAPI.addMessage("开始更新订阅商品本地化版本:\(product.productId)") let localizations = await ascAPI.fetchSubscriptionLocalizations(iapId: iap.id) @@ -508,16 +513,16 @@ extension APUploadIAPListVC { await createSubscriptionLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) } } - + // 4. 商品截图 await createSubscriptionScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 5. 销售国家或地区 await updateSubscriptionAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 3. 商品价格档位 await updateSubscriptionPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) - + } else { // 1. 创建新的商品 guard let iapGroupId = currentSubGroup?.id, let iap = await ascAPI.createSubscription(iapGroupId: iapGroupId, product: product) else { @@ -525,7 +530,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品:\(product.productId) ,创建失败!❌ ") return } - + // 2. 商品本地化语言 if let localization = product.subscriptionLocalization { await createSubscriptionLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) @@ -533,41 +538,41 @@ extension APUploadIAPListVC { // for localization in product.localizations { // await createSubscriptionLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) // } - - - + + + // 4. 商品截图 await createSubscriptionScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 5. 销售国家或地区 await updateSubscriptionAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) - + // 3. 商品价格档位 await updateSubscriptionPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) } - + ascAPI.addMessage("订阅商品:\(product.productId),\(product.name) ,上传完成!\n") } - - + + /// 更新订阅商品的价格档位 func updateSubscriptionPricePoint(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { guard let schedule = product.priceSchedules else { ascAPI.addMessage("无价格计划表:\(product.productId) ,请确认!❌ ") return } - + let baseTerritory = schedule.baseTerritory let baseCustomerPrice = schedule.baseCustomerPrice.normalizePrice() - + ascAPI.addMessage("开始更新订阅商品价格点,基准国家:\(product.productId),\(baseTerritory),\(baseCustomerPrice) \n") - + let isPreservePrice = preserveCurrentPriceBtn.state.rawValue == 1 ascAPI.addMessage("保留自动续期订阅者现有定价:\(isPreservePrice ? "是" : "否")") let points = await ascAPI.fetchSubscriptionPricePoints(iapId: iapId, territory: [baseTerritory]) if let point = points.filter({ $0.attributes?.customerPrice!.normalizePrice() == baseCustomerPrice }).first { - + ascAPI.addMessage("开始更新自定价格:") // 自定价格的国家或地区, 基准国家也算是自定价格 var customerPriceSchedules = schedule.manualPrices @@ -588,7 +593,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("自定价格的订阅商品价格点:\(territory),\(customerPrice) ,未找到此档位!❌ ") } } - + ascAPI.addMessage("开始更新全球均衡价格:") // 剩余的所有的国家地区的订阅价格点,然后一个一个设置。API不支持全部国家一次配置 let allPoints = await ascAPI.fetchSubscriptionPricePointsEqualizations(pointId: point.id, territory: nil) @@ -612,8 +617,8 @@ extension APUploadIAPListVC { ascAPI.addMessage("基准国家的订阅商品价格点:\(baseTerritory),\(baseCustomerPrice) ,未找到此档位!❌ ") } } - - + + /// 更新订阅商品的本地化信息 func createSubscriptionLocalization(iapId: String, localization: IAPLocalization, product: IAPProduct, ascAPI: APASCAPI) async { ascAPI.addMessage("开始更新订阅商品本地化版本:\(product.productId),\(localization.locale)") @@ -625,7 +630,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品本地化版本:\(localization.locale) ,更新语言失败!❌ ") } } - + /// 更新订阅商品的送审截图 func createSubscriptionScreenshot(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { ascAPI.addMessage("开始更新订阅商品的送审截图:\(product.productId),\(product.reviewScreenshot)") @@ -634,8 +639,9 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品:\(product.productId) 无送审截图或未上传截图~") return } - + let imaUrl = URL.init(fileURLWithPath: imgPath) + let uploadFileName = imaUrl.lastPathComponent.isEmpty ? imgName : imaUrl.lastPathComponent guard let fileMD5 = URL.init(fileURLWithPath: imgPath).fileMD5() else { ascAPI.addMessage("订阅商品截图文件错误:\(imgPath) ,无法生成 md5 值~") return @@ -650,16 +656,20 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品截图创建失败:\(imgName) ,无法删除旧截图~") } } - - ascAPI.addMessage("创建新的送审截图:\(product.reviewScreenshot)") + + ascAPI.addMessage("创建新的送审截图:\(uploadFileName)") // 创建截图 let imaSize = imaUrl.fileSizeInt() - guard let shot = await ascAPI.createSubscriptionScreenshot(iapId: iapId, fileName: imgName, fileSize: imaSize) else { + guard imaSize > 0 else { + ascAPI.addMessage("订阅商品截图文件错误:\(imgPath) ,文件大小为 0~") + return + } + guard let shot = await ascAPI.createSubscriptionScreenshot(iapId: iapId, fileName: uploadFileName, fileSize: imaSize) else { // 创建失败 ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!❌ ") return } - + // 根据苹果接口返回的上传接口上传 guard let method = shot.attributes?.uploadOperations?.first?.method, let url = shot.attributes?.uploadOperations?.first?.url, @@ -668,14 +678,14 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!苹果参数异常~ ❌ ") return } - + var request = URLRequest(url: baseURL) request.httpMethod = method for header in requestHeaders { request.headers[header.name ?? ""] = header.value ?? "" } - - ascAPI.addMessage("上传新的送审截图:\(product.reviewScreenshot)") + + ascAPI.addMessage("上传新的送审截图:\(uploadFileName)") // 上传图片 guard let response = try? await URLSession.shared.upload(for: request, fromFile: imaUrl) else { ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!上传图片异常~ ❌ ") @@ -685,8 +695,8 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!上传图片异常 \(response.1.description)~ ❌ ") return } - - ascAPI.addMessage("提交新的送审截图:\(product.reviewScreenshot)") + + ascAPI.addMessage("提交新的送审截图:\(uploadFileName)") // 确认图片 if ((await ascAPI.updateSubscriptionScreenshot(iapShotId: shot.id, fileMD5: fileMD5)) != nil) { ascAPI.addMessage("订阅商品:\(product.productId) ,送审截图上传成功!✅ ") @@ -694,7 +704,7 @@ extension APUploadIAPListVC { ascAPI.addMessage("订阅商品:\(product.productId) ,送审截图可能上传失败! ") } } - + /// 销售国家或地区 func updateSubscriptionAvailableTerritories(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { let inAll = product.territories.availableInAllTerritories @@ -702,7 +712,7 @@ extension APUploadIAPListVC { let summary = territoryInfo(product: product) let newTerritory = inNew ? "将来新国家(地区)时自动提供!" : "将来新国家(地区)时不自动提供!" ascAPI.addMessage("开始更新订阅商品的销售国家/地区:\(summary)") - + guard !inAll else { var allTerritories: [[String: String]] = [] if let territories = await ascAPI.territories() { @@ -723,7 +733,7 @@ extension APUploadIAPListVC { } return } - + /// 选择销售的国家或地区 var territories: [[String: String]] = [] product.territories.territories?.forEach({ territory in @@ -732,7 +742,7 @@ extension APUploadIAPListVC { "id": territory.id ]) }) - + let customerTerritory = product.territories.territories?.map({ $0.id }).joined(separator: ",") ?? "无" if (await ascAPI.updateSubscriptionAvailabilityTerritories(iapId: iapId, availableTerritories: territories, availableInNewTerritories: inNew)) != nil { ascAPI.addMessage("订阅商品的销售国家/地区:\(customerTerritory) ,更新成功!✅ ") @@ -744,7 +754,7 @@ extension APUploadIAPListVC { // MARK: - Privacy Method extension APUploadIAPListVC { - + func territoryInfo(product: IAPProduct) -> String { let inAll = product.territories.availableInAllTerritories let inNew = product.territories.availableInNewTerritories @@ -762,7 +772,7 @@ extension APUploadIAPListVC: NSTableViewDelegate, NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return iaps.count } - + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let iap = iaps[row] switch tableColumn?.identifier.enumValue() { @@ -815,7 +825,7 @@ extension APUploadIAPListVC: NSTableViewDelegate, NSTableViewDataSource { return nil } } - + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { let prices = iaps[row].priceSchedules?.manualPrices.count ?? 0 let count = max(prices + 2, 3) diff --git a/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift b/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift index f73e58d..75b9fa2 100644 --- a/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift +++ b/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift @@ -34,14 +34,14 @@ typealias ASCSubscriptionAvailability = AppStoreConnect_Swift_SDK.SubscriptionAv class APASCAPI { - + public var updateMsg: (([String])->Void)? private var message = [String]() { didSet { updateMsg?(message) } } - + // 日志文件保存路径 private let date = Date() var logPath: String { @@ -52,14 +52,14 @@ class APASCAPI { return "/AppleParty/ASCAPI/log_\(currentDate).txt" } } - + /// Go to https://appstoreconnect.apple.com/access/api and create your own key. This is also the page to find the private key ID and the issuer ID. /// Download the private key and open it in a text editor. Remove the enters and copy the contents over to the private key parameter. //private let configuration = APIConfiguration(issuerID: "", privateKeyID: "", privateKey: "") //private lazy var provider: APIProvider = APIProvider(configuration: configuration) private var provider: APIProvider? private var rateLimitPublisher: AnyCancellable? - + init(issuerID: String, privateKeyID: String, privateKey: String, showApiRateLimit: Bool) { do { let configuration = try APIConfiguration(issuerID: issuerID, privateKeyID: privateKeyID, privateKey: privateKey) @@ -73,7 +73,7 @@ class APASCAPI { handleError("初始化失败: \(error.localizedDescription)") } } - + /// 获取所有 app /// - Returns: apps func apps() async -> [ASCApp]? { @@ -97,7 +97,7 @@ class APASCAPI { return nil } } - + /// 获取所有 App Store 支持的国家或地区 /// - Returns: apps func territories() async -> [ASCTerritory]? { @@ -117,9 +117,9 @@ class APASCAPI { return nil } } - + // MARK: - 非续期订阅类型内购 API - + /// 获取 app 所有的商品信息 /// - Parameter appId: apple id /// - Returns: 商品列表 @@ -141,9 +141,9 @@ class APASCAPI { handleError("获取内购列表失败: \(error.localizedDescription)") return [] } - + } - + /// 创建内购商品 /// - Parameters: /// - appId: apple id @@ -172,7 +172,7 @@ class APASCAPI { "type": "inAppPurchases" ] ] - + do { guard let provider = provider else { return nil @@ -190,7 +190,7 @@ class APASCAPI { return nil } - + /// 修改内购商品 /// - Parameters: /// - iapId: 内购商品 id @@ -209,7 +209,7 @@ class APASCAPI { "type": "inAppPurchases" ] ] - + do { guard let provider = provider else { return nil @@ -227,7 +227,7 @@ class APASCAPI { return nil } - + /// 删除内购商品 /// - Parameters: /// - iapId: 内购商品 id @@ -248,8 +248,8 @@ class APASCAPI { } return 400 } - - + + /// 获取内购商品价格档位 /// - Parameters: /// - iapId: 内购商品 id @@ -276,8 +276,8 @@ class APASCAPI { return [] } } - - + + /// 构建一个价格计划表 /// - Parameters: /// - scheduleId: 价格计划表 id @@ -308,8 +308,8 @@ class APASCAPI { ] ] } - - + + /// 修改内购商品价格档位 /// - Parameters: /// - iapId: 内购商品 id @@ -371,7 +371,7 @@ class APASCAPI { // ] // ] ] - + do { guard let provider = provider else { return nil @@ -388,8 +388,8 @@ class APASCAPI { } return nil } - - + + /// 获取内购商品本地化信息 /// - Parameters: /// - iapId: 内购商品 id @@ -413,8 +413,8 @@ class APASCAPI { return [] } } - - + + /// 创建内购商品本地化 /// - Parameters: /// - iapId: 内购商品 id @@ -439,7 +439,7 @@ class APASCAPI { "type": "inAppPurchaseLocalizations" ] ] - + do { guard let provider = provider else { return nil @@ -457,7 +457,7 @@ class APASCAPI { return nil } - + /// 修改内购商品本地化信息 /// - Parameters: /// - iapLocaleId: 内购商品本地化语言 id @@ -491,7 +491,7 @@ class APASCAPI { return nil } - + /// 删除内购商品本地化 /// - Parameters: /// - iapLocaleId: 内购商品本地化语言 id @@ -512,8 +512,8 @@ class APASCAPI { } return 400 } - - + + /// 获取内购商品的送审截屏 /// - Parameter iapId: 内购商品 id /// - Returns: 返回内购商品的截图信息 @@ -530,7 +530,7 @@ class APASCAPI { return nil } } - + /// 创建内购商品的送审截屏 /// - Parameters: /// - iapId: 内购商品 id @@ -555,7 +555,7 @@ class APASCAPI { "type": "inAppPurchaseAppStoreReviewScreenshots" ] ] - + do { guard let provider = provider else { return nil @@ -572,8 +572,8 @@ class APASCAPI { } return nil } - - + + /// 确认内购商品的送审截屏 /// - Parameters: /// - iapShotId: 内购商品截屏 id @@ -590,7 +590,7 @@ class APASCAPI { "type": "inAppPurchaseAppStoreReviewScreenshots" ] ] - + do { guard let provider = provider else { return nil @@ -607,7 +607,7 @@ class APASCAPI { } return nil } - + /// 删除内购商品的送审截屏 /// - Parameters: /// - iapShotId: 内购商品截屏 id @@ -628,8 +628,8 @@ class APASCAPI { } return 400 } - - + + /// 修改内购商品的销售范围 /// - Parameters: /// - iapId: 内购商品 id @@ -662,7 +662,7 @@ class APASCAPI { ] ] ] - + do { guard let provider = provider else { return nil @@ -679,11 +679,11 @@ class APASCAPI { } return nil } - - + + // MARK: - 续期订阅类商品 API - - + + /// 获取所有订阅组 /// - Parameters: /// - appId: apple id @@ -707,8 +707,8 @@ class APASCAPI { return [] } } - - + + /// 创建订阅组 /// - Parameters: /// - appId: apple id @@ -731,7 +731,7 @@ class APASCAPI { "type": "subscriptionGroups" ] ] - + do { guard let provider = provider else { return nil @@ -748,8 +748,8 @@ class APASCAPI { } return nil } - - + + /// 获取所有订阅组的本地化信息 /// - Parameters: /// - iapGroupId: 订阅组 id @@ -773,8 +773,8 @@ class APASCAPI { return [] } } - - + + /// 创建订阅组本地化信息 /// - Parameters: /// - iapGroupId: 订阅组 id @@ -801,7 +801,7 @@ class APASCAPI { "type": "subscriptionGroupLocalizations" ] ] - + do { guard let provider = provider else { return nil @@ -818,8 +818,8 @@ class APASCAPI { } return nil } - - + + /// 获取订阅组下所有内购商品 /// - Parameters: /// - appId: apple id @@ -843,8 +843,8 @@ class APASCAPI { return [] } } - - + + /// 创建订阅商品 /// - Parameters: /// - iapGroupId: apple id @@ -873,7 +873,7 @@ class APASCAPI { "type": "subscriptions" ] ] - + do { guard let provider = provider else { return nil @@ -890,8 +890,8 @@ class APASCAPI { } return nil } - - + + func updateSubscription(iapId: String, product: IAPProduct) async -> ASCSubscription? { let body = [ "data": [ @@ -923,8 +923,8 @@ class APASCAPI { } return nil } - - + + /// 获取订阅商品本地化信息 /// - Parameters: /// - iapId: 内购商品 id @@ -949,8 +949,8 @@ class APASCAPI { } } - - + + /// 创建订阅商品本地化 /// - Parameters: /// - iapId: 内购商品 id @@ -975,7 +975,7 @@ class APASCAPI { "type": "subscriptionLocalizations" ] ] - + do { guard let provider = provider else { return nil @@ -993,7 +993,7 @@ class APASCAPI { return nil } - + /// 修改订阅商品本地化信息 /// - Parameters: /// - iapLocaleId: 内购商品本地化语言 id @@ -1027,8 +1027,8 @@ class APASCAPI { } return nil } - - + + /// 获取订阅商品价格档位 /// - Parameters: /// - iapId: 内购商品 id @@ -1055,8 +1055,8 @@ class APASCAPI { return [] } } - - + + /// 获取订阅商品价格档位的等价价格 /// - Parameters: /// - pointId: 内购价格档位 id @@ -1083,8 +1083,8 @@ class APASCAPI { return [] } } - - + + /// 修改订阅商品价格档位 /// - Parameters: /// - iapId: 内购商品 id @@ -1115,7 +1115,7 @@ class APASCAPI { ] ] ] - + do { guard let provider = provider else { return nil @@ -1132,8 +1132,8 @@ class APASCAPI { } return nil } - - + + /// 获取订阅商品的送审截屏 func fetchSubscriptionScreenshot(iapId: String) async -> ASCSubscriptionScreenshot? { let request = APIEndpoint.v1.subscriptions.id(iapId).appStoreReviewScreenshot.get() @@ -1143,12 +1143,23 @@ class APASCAPI { } let shot = try await provider.request(request).data return shot + } catch APIProvider.Error.decodingError(let decodeError, let data) { + if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + json["data"] is NSNull { + return nil + } + let err = APIProvider.Error.decodingError(decodeError, data) + handleError("获取订阅商品的送审截图异常: \(err.localizedDescription)") + return nil + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + return nil } catch { handleError("获取订阅商品的送审截图异常: \(error.localizedDescription)") return nil } } - + /// 创建订阅商品的送审截屏 func createSubscriptionScreenshot(iapId: String, fileName: String, fileSize: Int) async -> ASCSubscriptionScreenshot? { let body = [ @@ -1203,7 +1214,7 @@ class APASCAPI { "type": "subscriptionAppStoreReviewScreenshots" ] ] - + do { guard let provider = provider else { return nil @@ -1220,7 +1231,7 @@ class APASCAPI { } return nil } - + /// 删除订阅商品的送审截屏 /// - Parameters: /// - iapShotId: 内购商品截屏 id @@ -1241,8 +1252,8 @@ class APASCAPI { } return 400 } - - + + /// 修改订阅商品的销售范围 /// - Parameters: /// - iapId: 内购商品 id @@ -1275,7 +1286,7 @@ class APASCAPI { ] ] ] - + do { guard let provider = provider else { return nil @@ -1297,30 +1308,30 @@ class APASCAPI { // MARK: - handle log extension APASCAPI { - + func handleRequestFailure(_ statusCode: Int, _ errorResponse: ErrorResponse?) { print("Request failed with statuscode: \(statusCode) and the following errors:") errorResponse?.errors?.forEach({ error in handleError("Error code: \(error.code), title: \(error.title), detail: \(String(describing: error.detail))") }) } - + func handleError(_ msg: String) { addMessage(msg) print(msg) } - + func addMessage(_ msg: String) { let dateFormatter : DateFormatter = DateFormatter() dateFormatter.dateFormat = "[MM-dd HH:mm:ss] " let currentDateString = dateFormatter.string(from: Date()) let log = currentDateString + msg message.append(log) - + // 本地保存 saveLogs(log: log) } - + func saveLogs(log: String, retry: Int = 3) { do { try log.appendLine(to: logPath.createFilePath)