jsdw_ios/QuickLocation/Manager/App/ApiManager.swift

304 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ApiManager.swift
// SHECommunity
//
// Created by 林 on 2024/11/26.
//
import SwiftyUserDefaults
import Moya
import SwiftyJSON
import RxSwift
import CommonCrypto
import UIKit
#if !targetEnvironment(simulator)
import GeYanSdk
#endif
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-package"] = "cn.zuom8.jisuloca"
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()
// 机型
headers["x-mobile-model"] = UIDevice.machineIdentifier
if let loginToken = Defaults[\.loginToken] {
headers["x-token"] = loginToken
}
return headers
}
// MARK: - 接口验证
private func handleResult(_ result: Result<Response, MoyaError>, handle: Bool) -> Result<Response, MoyaError>? {
DLToast.dismiss()
switch result {
case let .success(response):
let isPlayback = response.request?.url?.absoluteString.contains("mapi/trips/playback") ?? false
// 处理加密响应
let t0 = CFAbsoluteTimeGetCurrent()
let decryptedData = ApiManager.decryptIfNeeded(response.data)
if isPlayback {
let rawSize = response.data.count
let decryptTime = CFAbsoluteTimeGetCurrent() - t0
print("[playback] raw: \(rawSize)B, decrypt: \(String(format: "%.2f", decryptTime))s")
}
// response转json
let json = JSON(decryptedData ?? response.data)
if !isPlayback {
print("============== decrypt ====================")
print(json)
}
let code = json["code"].int
let success: Bool? = json["success"].bool
let message = json["message"].string?
.split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
.first
.map(String.init)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "请检查网络连接"
// 接口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,
GatewayStatusCode.userLoginExpair2.rawValue,
GatewayStatusCode.userLoginExpair3.rawValue: // token失效
handleTokenExpired(msg: message)
case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退出当前界面
handlePopView(message, handle)
default:
// 接口没返回code的时候取success字段
if success != nil {
if let decrypted = decryptedData {
let decryptedResponse = Response(statusCode: response.statusCode,
data: decrypted,
request: response.request, response: response.response)
return .success(decryptedResponse)
}
return result
}
/// 统一提示错误
let code = GatewayStatusCode(rawValue: code ?? -9999) ?? .unknownError
if code == .unknownError, handle {
DLToast.show(text: message)
}
}
return .failure(handleError(with: code,
domain: "Data Error",
message: message,
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 {
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
GeYanSdk.deletePreResultCache()
AppContextManager.shared.deleteAccount()
GroupIMService.shared.logout()
DLToast.showError(text: msg ?? "登录失效,请重新登录!") { [weak self] in
guard let self else { return }
SystemService.userConfig().subscribe(onNext: { response in
guard let model = response.model else { return }
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
DLToast.dismiss {
AppDelegate.shared.showMainViewController()
}
}, onError: { _ in }).disposed(by: self.disposeBag)
}
}
}
// MARK: - 业务中断(需退出当前页面)
private func handlePopView(_ msg: String?, _ handle: Bool) {
MainAsync {
guard handle else { return }
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 decryptedData = aes256CBCDecrypt(encryptedString, key: jieMiKey, iv: jieMiIV)
else { return nil }
// 验证解密结果是否为合法 JSON
guard (try? JSONSerialization.jsonObject(with: decryptedData)) != nil else { return nil }
return decryptedData
}
private class func aes256CBCDecrypt(_ base64String: String, key: String, iv: String) -> Data? {
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 Data(bytes: buffer, count: numBytesDecrypted)
}
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..<Int(CC_MD5_DIGEST_LENGTH) {
result += String(format: "%02x", digest[i])
}
return isUppercase ? result.uppercased() : result
}
class func getFileTime() -> 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
}
}