// // MP_LuxServerManager.swift // relax.offline.mp3.music // // Created by Mr.Zhou on 2024/9/10. // import UIKit import Alamofire import Security import AdSupport ///自家后台管理器 class MP_LuxServerManager: NSObject { //单例工具 static let shared = MP_LuxServerManager() ///keychain关键词 private let service = "relax.offline.mp3.music.deviceIdentifier" //MARK: - URL ///基础链接 private let baseUrl:String = "https://openapi.lux-ad.com" ///报活链接 private let activeUrl:String = "/statistic/appdatacollection/saveAppData" ///报错链接 private let errorUrl:String = "/statistic/applogscollection/save" //MARK: - 通用参数值 ///唯一的设备UUID(重装应用后,也会使用该设备ID) private var uuID:String{ get{ return getDeviceUUID() } } ///用户ID(每次重装应用都会生成一个新的用户ID) private var userID:String{ get{ return getUserID() } } ///IDFA广告跟踪ID(检索用户是否具备权限,无权限/位置状态返回内容则是由0构成的字符串;具备权限则会返回正常的跟踪ID) private let IDFAID:String = ASIdentifierManager.shared().advertisingIdentifier.uuidString ///渠道 private let channel:String = "ios" ///包名 private let pkgName:String = "relax.offline.mp3.music" ///事件名 private var eventName:String { get{ return getEventName() } } ///当前时间节点毫秒数 private var timestamp:Int64 { get{ //获取当前时间值 let now = Date() //获取时间缀,并转为毫秒级别 let currentTimestampInMillisInt:Int64 = Int64(now.timeIntervalSince1970 * 1000) return currentTimestampInMillisInt } } /// 设备类型 private var deviceVersion: String { return UIDevice.current.modelName } ///系统版本 private var osVersion:String { return UIDevice.current.systemVersion } //MARK: - 会话 ///后台会话 private lazy var LuxSession:Session = { let configuration = URLSessionConfiguration.af.default ///超时设置 configuration.timeoutIntervalForRequest = 20 configuration.timeoutIntervalForResource = 20 // 可调整网络服务类型 configuration.networkServiceType = .default let seesion = Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier()) return seesion }() override init() { super.init() } ///报活参数结构 /** { "eventName": "app_open", "timestamp": 1711522237247, "uuid": "c1c4c016cd6276b41e2447653fe5a9b2", "app_version": "1.5.4", "channel": "google", "country": "cn", "device": "android", "language": "zh", "pkgName": "com.master.ae.safevpn", "userId": "55a7fa0c-be84-44b9-a73e-a4256dbb30ee", "adId": "995772a9-7748-418e-bef4-10ca1ad1abe1", "createdOn": "2022-03-10T12:15:50.000Z", "data": { "property1": {}, "property2": {} } }**/ //MARK: - 报活操作 ///实现报活会话 func upDateOpenActiveEventTask() { //生成报活链接 guard let url = URL(string: baseUrl+activeUrl) else { return } //生成参数 let parameters:[String:Any] = [ "userId":userID, "ad_id":IDFAID, "uuid":uuID, "device":deviceVersion, "appVersion":app_Version, "language":Language_first_local, "channel":channel, "pkgName":pkgName, "eventName":eventName, "country":Language_first_local, "dataStr":"Active", "timestamp":timestamp ] postUpDateOpenActiveEvent(url, parameters: parameters){ [weak self] statu in guard let self = self, statu == true else {return} //检索更多报活信息,并执行报活任务 var loads = loadPendingActivities() guard loads.isEmpty == false else {return} var indicesToRemove: [Int] = [] let dispatchGroup = DispatchGroup() for (index, item) in loads.enumerated() { dispatchGroup.enter() postUpDateOpenActiveEvent(url, parameters: item, isRepeat: true) { [weak self] statu in guard let self = self, statu == true else { dispatchGroup.leave() return } //报活成功,将成功的报活信息移除 indicesToRemove.append(index) dispatchGroup.leave() } } dispatchGroup.notify(queue: .main) { [weak self] in guard let self = self else {return} // 删除收集的索引 for index in indicesToRemove.sorted(by: >) { loads.remove(at: index) } reloadActivities(loads) } } } //执行报活 private func postUpDateOpenActiveEvent(_ url:URL, parameters:[String:Any], isRepeat:Bool = false, completion:((Bool) -> Void)?) { LuxSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonActive.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): print("成功报活:\(value)") completion?(true) case .failure(let error): print("报活失败,失败错误:\(error.localizedDescription)") if isRepeat == false { //非重复信息,报活信息存入表中 saveActivity(parameters) } completion?(false) } } } //保存未加载 private func saveActivity(_ parameters:[String:Any]) { var pendingActivities = loadPendingActivities() pendingActivities.append(parameters) if let encoded = try? JSONSerialization.data(withJSONObject: pendingActivities, options: []) { UserDefaults.standard.set(encoded, forKey: "relax.offline.mp3.music.Activities") } } //更新未加载信息 private func reloadActivities(_ loads:[[String:Any]]) { if let encoded = try? JSONSerialization.data(withJSONObject: loads, options: []) { UserDefaults.standard.set(encoded, forKey: "relax.offline.mp3.music.Activities") } } //加载未上传的报活信息 private func loadPendingActivities() -> [[String:Any]] { if let savedData = UserDefaults.standard.data(forKey: "relax.offline.mp3.music.Activities"), let decoded = try? JSONSerialization.jsonObject(with: savedData, options: []) as? [[String: Any]] { return decoded } return [] } //MARK: - 设备ID ///获得设备ID private func getDeviceUUID() -> String { if let uuid = loadUUIDFromKeychain() { return uuid } else { let newUUID = UUID().uuidString saveUUIDToKeychain(uuid: newUUID) return newUUID } } /// 从 Keychain 中加载 UUID private func loadUUIDFromKeychain() -> String? { let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne] var dataTypeRef: AnyObject? = nil let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) if status == errSecSuccess { if let data = dataTypeRef as? Data, let uuid = String(data: data, encoding: .utf8) { return uuid } } return nil } /// 保存 UUID 到 Keychain private func saveUUIDToKeychain(uuid: String) { if let data = uuid.data(using: .utf8) { let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecValueData as String: data] SecItemAdd(query as CFDictionary, nil) } } //MARK: - 用户ID ///获取用户ID private func getUserID() -> String { //检索Userdefaults的userID if let userID = UserDefaults.standard.string(forKey: "relax.offline.mp3.music.userIdentifier") { return userID }else { let newUUID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString //存入UserDefaults UserDefaults.standard.set(newUUID, forKey: "relax.offline.mp3.music.userIdentifier") return newUUID } } ///报活事件名 private func getEventName() -> String { //检索 if UserDefaults.standard.string(forKey: "relax.offline.mp3.music.firstEvent") != nil { return "app_open" }else { UserDefaults.standard.set("first_open", forKey: "relax.offline.mp3.music.firstEvent") return "first_open" } } //MARK: - 报错操作 //上传报错日志 func updateErrorLogEventTask(_ level:ErrorLevel, title:String, message:String) { //生成报错链接 guard let url = URL(string: baseUrl+errorUrl) else { return } //生成参数 let parameters:[String:Any] = [ "pkgName":pkgName, "appVersion":app_Version, "os":osVersion, "device":deviceVersion, "Level":level.rawValue, "title":title, "message":message, ] } } ///报活信息结构 struct JsonActive:Codable { let message:String? let status:String? enum CodingKeys: String, CodingKey { case message = "message" case status = "status" } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) message = try values.decodeIfPresent(String.self, forKey: .message) status = try values.decodeIfPresent(String.self, forKey: .status) } } ///报错类型 enum ErrorLevel:Int { case debug = 1 case info = 2 case warn = 3 case error = 4 case crash = 5 }