306 lines
11 KiB
Swift
306 lines
11 KiB
Swift
//
|
||
// 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.name
|
||
}
|
||
///系统版本
|
||
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
|
||
}
|