// // ApiManager.swift // SHECommunity // // Created by 林 on 2024/11/26. // import SwiftyUserDefaults import Moya import SwiftyJSON import RxSwift import CommonCrypto class ApiManager: NSObject { public static let shared = ApiManager() let disposeBag = DisposeBag() var ignoreCheck: Bool = false /// firebase var fcmToken: String? } // MARK: - NetworkConfig extension ApiManager { /// 初始化 func setup() { setupNetworkConfig() } /// 初始化网络配置 func setupNetworkConfig() { AppNetworkConfig.shared.baseURL = URLManager.shared.apiServerURL AppNetworkConfig.shared.httpHeader = { [weak self] in self?.httpHeader() ?? [:] } AppNetworkConfig.shared.handleRespone = { [weak self] (result, isShowErrorToast) in guard let this = self else { return nil } return this.handleResult(result, handle: isShowErrorToast) } } /// HttpHeader func httpHeader() -> [String: String] { var headers: [String : String] = [:] headers["Accept"] = "application/json" // headers["Content-Type"] = "application/json" headers["x-platform"] = "ios" headers["x-channel"] = "ios" headers["x-mobile-brand"] = "ios" headers["x-app-id"] = "10057" headers["x-version"] = kAppShortVersion headers["x-device-id"] = ApiManager.getMD5ID() // 机型 var systemInfo = utsname() uname(&systemInfo) let platform = withUnsafePointer(to: &systemInfo.machine.0) { ptr in return String(cString: ptr) } headers["x-mobile-model"] = platform if let loginToken = Defaults[\.loginToken] { headers["x-token"] = loginToken } return headers } // MARK: - 接口验证 private func handleResult(_ result: Result, handle: Bool) -> Result? { DLToast.dismiss() switch result { case let .success(response): // 处理加密响应 let decryptedData = ApiManager.decryptIfNeeded(response.data) // response转json let json = JSON(decryptedData ?? response.data) print("============== decrypt ====================") print(json) let code = json["code"].int let message = json["message"].string let combineMessage = message ?? "请检查网络连接" // 接口code switch code { case GatewayStatusCode.success.rawValue: // 如果有解密数据,返回解密后的 Response if let decrypted = decryptedData { let decryptedResponse = Response(statusCode: response.statusCode, data: decrypted, request: response.request, response: response.response) return .success(decryptedResponse) } return result // case GatewayStatusCode.outdateVersion.rawValue: // 版本过低 // handleOutdateVersion(message) case GatewayStatusCode.userLoginExpair.rawValue: // token失效 handleTokenExpired(msg: message) case GatewayStatusCode.noAuthority.rawValue: // 退出当前界面 handlePopView(message) default: /// 统一提示错误 let code = GatewayStatusCode(rawValue: code ?? -9999) ?? .unknownError if code == .unknownError, handle { DLToast.show(text: combineMessage) } } return .failure(handleError(with: code, domain: "Data Error", message: combineMessage, data: response)) case let .failure(error): let message = error.gatewayMessage ?? "请检查网络连接" if error.innerError?.code == NSURLErrorCancelled { // 处理手动取消请求,打印日志 LogInfo("cancel response: error: \(error.localizedDescription)") } else if handle { DLToast.show(text: message) } let innerError = error.innerError ?? error as NSError return .failure(handleError(with: innerError.code, domain: innerError.domain, message: message)) } } private func handleError(with code: Int?, domain: String, message: String, isShowTip: Bool = true, data: Response? = nil) -> MoyaError { // 处理错误信息 let responseError = NSError(domain: domain, code: code ?? -999999, userInfo: [NSLocalizedDescriptionKey: message, "isShowTip": isShowTip]) let error = MoyaError.underlying(responseError, data) return error } // MARK: - 处理Token过期 private func handleTokenExpired(msg: String?) { MainAsync { AppContextManager.shared.deleteAccount() // IMServiceManager.shared.logout() DLToast.showError(text: msg ?? "登录失效,请重新登录!") { // AppDelegate.shared.showMainViewController() } } } // MARK: - 业务中断(需退出当前页面) private func handlePopView(_ msg: String?) { MainAsync { DLToast.showInfo(text: msg ?? "Error...") { guard let viewController = UIViewController.currentViewController() else { return } viewController.navigationController?.popViewController(animated: true) } } } } extension ApiManager { // MARK: - AES-256-CBC 解密 private static let jieMiKey = "0b0d7962d67a76f00cf9fe0eec0061c2" private static let jieMiIV = String(jieMiKey.prefix(16)) /// 响应解密:如果 encrypt == 1,则解密 data 字段并返回 JSON Data class func decryptIfNeeded(_ responseData: Data) -> Data? { guard let json = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any], let encrypt = json["encrypt"] as? Int, encrypt == 1, let encryptedString = json["data"] as? String, let decryptedString = aes256CBCDecrypt(encryptedString, key: jieMiKey, iv: jieMiIV), let decryptedData = decryptedString.data(using: .utf8), let decryptedJSON = try? JSONSerialization.jsonObject(with: decryptedData) as? [String: Any] else { return nil } return try? JSONSerialization.data(withJSONObject: decryptedJSON) } private class func aes256CBCDecrypt(_ base64String: String, key: String, iv: String) -> String? { guard let data = Data(base64Encoded: base64String), let keyData = key.data(using: .utf8), let ivData = iv.data(using: .utf8) else { return nil } let bufferSize = data.count + kCCBlockSizeAES128 var buffer = [UInt8](repeating: 0, count: bufferSize) var numBytesDecrypted = 0 let status = keyData.withUnsafeBytes { keyBytes in ivData.withUnsafeBytes { ivBytes in data.withUnsafeBytes { dataBytes in CCCrypt( CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithmAES), CCOptions(kCCOptionPKCS7Padding), keyBytes.baseAddress, kCCKeySizeAES256, ivBytes.baseAddress, dataBytes.baseAddress, data.count, &buffer, bufferSize, &numBytesDecrypted ) } } } guard status == kCCSuccess else { return nil } return String(data: Data(bytes: buffer, count: numBytesDecrypted), encoding: .utf8) } class func getMD5ID() -> String { var strback = "" strback = "\(getFileTime())-\(getSysU())" if strback.count < 6 { strback = UIDevice.current.identifierForVendor?.uuidString ?? "" } return getMd5_32Bit_String(strback, isUppercase: false) } class func getMd5_32Bit_String(_ srcString: String, isUppercase: Bool) -> String { let cStr = srcString.cString(using: .utf8) let length = CC_LONG(strlen(cStr!)) var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) CC_MD5(cStr, length, &digest) var result = "" for i in 0.. String { var info = stat() let result = stat("/var/mobile", &info) guard result == 0 else { return "" } let time = info.st_birthtimespec return String(format: "%ld.%09ld", time.tv_sec, time.tv_nsec) } class func getSysU() -> String? { let information = "L3Zhci9tb2JpbGUvTGlicmFyeS9Vc2VyQ29uZmlndXJhdGlvblByb2ZpbGVzL1B1YmxpY0luZm8vTUNNZXRhLnBsaXN0" guard let data = Data(base64Encoded: information), let dataString = String(data: data, encoding: .utf8) else { return nil } do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: dataString) if let date = fileAttributes[.creationDate] as? Date { return String(format: "%.6f", date.timeIntervalSince1970) } } catch { print("获取文件属性失败:\(error)") } return nil } }