264 lines
9.8 KiB
Swift
264 lines
9.8 KiB
Swift
//
|
||
// 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-package"] = Bundle.main.bundleIdentifier
|
||
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<Response, MoyaError>, handle: Bool) -> Result<Response, MoyaError>? {
|
||
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..<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
|
||
}
|
||
}
|