diff --git a/YXTrackingSDK/YXTracking.swift b/YXTrackingSDK/YXTracking.swift new file mode 100644 index 0000000..e8ebb17 --- /dev/null +++ b/YXTrackingSDK/YXTracking.swift @@ -0,0 +1,415 @@ +// +// YXTracking.swift +// YXTrackingSDK +// +// Created by 1234 on 2026/1/27. +// + +import UIKit + +/// 远轩埋点SDK实现类 四种上报机制 +/// 1、初始化时触发:用户在初始化SDK时,会将缓存的事件数据上报。 +/// 2、定时/定量触发:数据条数达到阈值(默认20条)或定时器到期(默认600秒)时会将缓存的事件数据上报。 +/// 3、事件触发:用户在添加事件时可选择立即上报。 +/// 4、应用进入后台和即将被系统终止时触发:用户在应用内停留一段时间后,手动切换到其他应用或桌面时,或即将被系统终止时,会将缓存的事件数据上报。 +public class YXTracking: NSObject { + + static let shared = YXTracking() + + private var host: String = "" // 主机地址 + + private var cacheCount: Int = 20 // 缓存数量 + + private var pushInterval: Int = 600 // 上报周期(单位:秒) + + private var enableLog: Bool = false // 是否开启调试日志 + + private var eventRequestResult: EventRequestResult = EventRequestResult() // 事件请求参数 + + private var systemAllDimInfoResponseResult: SystemAllDimInfoResponseResult = SystemAllDimInfoResponseResult() // 系统所有维度信息 + + /// 初始化SDK + /// - Parameters: + /// - host: 主机地址 + /// - systemCode: 系统编码 + /// - cacheCount: 缓存数量(可选) + /// - pushInterval: 上报周期(可选) + /// - enableLog: 是否开启调试日志(可选) + /// - Returns: 无返回值 + public class func initWithSystemCode(host: String, systemCode: String, cacheCount: Int?, pushInterval: Int?, enableLog: Bool?) { + // 校验主机地址是否为空 + if host.isEmpty { + YXTracking.shared.error(message: "SDK config failed, host is empty!"); + return; + } + // 校验系统编码是否为空 + if systemCode.isEmpty { + YXTracking.shared.error(message: "SDK config failed, system code is empty!"); + return; + } + + // 设置主机地址和缓存数量、上报周期、是否开启调试日志 + YXTracking.shared.host = host + if cacheCount != nil { + YXTracking.shared.cacheCount = cacheCount! + } + if pushInterval != nil { + YXTracking.shared.cacheCount = pushInterval! + } + if enableLog != nil { + YXTracking.shared.enableLog = enableLog! + } + + // 打印日志信息 + YXTracking.shared.log(message: "SDK config success!"); + + // 添加应用进入后台和即将被系统终止的通知监听 应用进入后台时或即将被系统终止时触发缓存事件推送 + YXTracking.shared.addEnterBackgroundNotificationObserver() + YXTracking.shared.addWillTerminateNotificationObserver() + + // 重置事件请求参数中的系统编码和客户端类型 + YXTracking.shared.eventRequestResult.system_code = systemCode + YXTracking.shared.eventRequestResult.clientType = 2 + + // 获取设备信息 + YXTracking.shared.getDeviceInfo() + + // 获取系统所有维度信息 + YXTracking.shared.getSystemAllDimInfo() + } + + /// 添加事件 + /// - Parameters: + /// - eventType: 事件类型 + /// - page: 事件页面controller名称 + /// - buttonId: 事件按钮名称 + /// - url: 事件action + /// - customTags: 自定义标签数组(可选) + /// - pushNow: 是否立即推送 + /// - Returns: 无返回值 + public class func addEvent(eventType: String, page: String, buttonId: String, url: String, customTags: Array?, pushNow: Bool) { + // 校验事件类型是否在系统维度信息中存在 + if let systemEventTypes = YXTracking.shared.systemAllDimInfoResponseResult.systemEventTypes, + systemEventTypes.count > 0 { + guard !eventType.isEmpty else { + YXTracking.shared.error(message: "SDK push event error, event type is empty!"); + return + } + + if !systemEventTypes.contains(where: { $0.event_code == eventType }) { + YXTracking.shared.error(message: "SDK push event error, event type not in systemEventTypes!"); + return; + } + } else { + YXTracking.shared.error(message: "SDK push event error, systemEventTypes is empty!"); + return + } + + // 重置事件请求参数中的事件类型 + YXTracking.shared.eventRequestResult.eventType = eventType + + // 重置事件请求参数中的事件参数 + let eventParams = EventParams(page: page, buttonId: buttonId, url: url) + YXTracking.shared.eventRequestResult.eventParams = eventParams + + // 重置事件请求参数中的自定义标签 + YXTracking.shared.eventRequestResult.customTags = customTags + + // 重置事件请求参数中的时间戳 + YXTracking.shared.getTimestamp() + + // 打印日志信息 + YXTracking.shared.log(message: "SDK add event success\nEvent Request Parameter:\n\(YXTracking.shared.eventRequestResult)"); + + // 缓存事件数据或立即推送事件数据 + if pushNow { + YXTracking.shared.pushEvent(event: YXTracking.shared.eventRequestResult) + } else { + YXTrackingSQLiteManager.addTrackingData(data: YXTracking.shared.eventRequestResult) + YXTracking.shared.checkEventCount() + } + } + + /// 设置用户信息 + /// - Parameters: + /// - userInfo: 用户信息 + /// - Returns: 无返回值 + public class func setUserInfo(userId: Int64, userName: String, account: String) { + let userInfo = UserInfo(userId: userId, userName: userName, account: account) + YXTracking.shared.eventRequestResult.userInfo = userInfo + + YXTracking.shared.log(message: "SDK config user info success\nUser info:\n\(userInfo)"); + } + + /// 清空用户信息 + public class func clearUserInfo() { + YXTracking.shared.eventRequestResult.userInfo = nil + + YXTracking.shared.log(message: "SDK clear user info success!"); + } + + /// 获取设备信息 + func getDeviceInfo() { + let device = UIDevice.current + + let os = "\(device.systemName) \(device.systemVersion)" + + let localizedModel = device.localizedModel + + let screenWidthPoints = UIScreen.main.bounds.width + let screenHeightPoints = UIScreen.main.bounds.height + let scale = UIScreen.main.scale + let screenWidthPixels = screenWidthPoints * scale + let screenHeightPixels = screenHeightPoints * scale + let screenResolution = "\(Int(screenWidthPixels))x\(Int(screenHeightPixels))" + + let deviceInfo = DeviceInfo(os: os, model: localizedModel, screenResolution: screenResolution) + YXTracking.shared.eventRequestResult.deviceInfo = deviceInfo + + YXTracking.shared.debug(message: "SDK config device info success\nDevice info:\n\(deviceInfo)"); + } + + /// 获取时间戳 + func getTimestamp() { + let currentDate = Date() + + let unixTimestamp = currentDate.timeIntervalSince1970 + let milliseconds = Int64(unixTimestamp * 1000) + + let customFormatter = ISO8601DateFormatter() + customFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + customFormatter.timeZone = TimeZone.current + let isoStringWithMilliseconds = customFormatter.string(from: currentDate) + + YXTracking.shared.eventRequestResult.clientTimestamp = milliseconds + YXTracking.shared.eventRequestResult.timestamp = isoStringWithMilliseconds + } + + /// 判断数据库数量是否达到缓存上限,如已达到,则推送缓存事件 + func checkEventCount() { + let count = YXTrackingSQLiteManager.getTrackingDataCount() + if count >= cacheCount { + YXTracking.pushEventList() + } + } + + /// 开启定时推送本地缓存的事件日志列表 + func startThreadPool() { + let queue = DispatchQueue(label: "com.yx.tracking", qos: .userInitiated) + queue.async { [self] in + while true { + YXTracking.pushEventList() + sleep(UInt32(pushInterval)) + } + } + } + + /// 根据系统编码获取系统所有维度信息 + func getSystemAllDimInfo() { + let systemCode = YXTracking.shared.eventRequestResult.system_code ?? "" + guard !systemCode.isEmpty else { return } + + let urlString = "\(host)/api/ExternalEventlogs/GetSystemAllDimInfo?systemCode=\(systemCode)" + guard let url = URL(string: urlString) else { + return + } + + let task = URLSession.shared.dataTask(with: url) { [self] data, response, error in + if let error = error { + YXTracking.shared.error(message: error.localizedDescription) + return + } + + guard let data = data else { + return + } + + let json = try? JSONSerialization.jsonObject(with: data, options: []) + if let dictionary = json as? [String: Any] { + if let code = dictionary["code"] as? Int, code == 200 { + if let result = dictionary["data"] as? Dictionary, + let resultData = try? JSONSerialization.data(withJSONObject: result) { + do { + let resultModel = try JSONDecoder().decode(SystemAllDimInfoResponseResult.self, from: resultData) + systemAllDimInfoResponseResult = resultModel + + YXTracking.shared.log(message: "SDK init success\nSystem All Dim Info:\n\(systemAllDimInfoResponseResult)"); + + // 初始化成功后 推送本地缓存的事件日志列表 + YXTracking.pushEventList() + + // 开启线程池,定时推送本地缓存的事件日志列表 + YXTracking.shared.startThreadPool() + } catch { + YXTracking.shared.error(message: "SDK init fail!"); + } + } else { + YXTracking.shared.error(message: "无法创建Data对象!") + } + } else { + YXTracking.shared.error(message: "SDK init fail,error code: \(dictionary["code"] ?? "未知")") + } + } + } + task.resume() + } + + /// 推送单条日志 + /// - Parameters: + /// - event: 事件对象 + /// - Returns: 无返回值 + func pushEvent(event: EventRequestResult) { + let urlString = "\(host)/api/ExternalEventlogs/AddEventLog" + guard let url = URL(string: urlString) else { + return + } + + // 创建URL请求 + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // 将event对象转换为JSON数据 + let jsonEncoder = JSONEncoder() + do { + let jsonData = try jsonEncoder.encode(event) + request.httpBody = jsonData + } catch { + YXTracking.shared.error(message: "SDK push event error encoding\n\(error)"); + return + } + + // 发送请求 + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + YXTracking.shared.error(message: "SDK push event error sending\n\(error)"); + return + } + + guard let data = data else { + return + } + + let json = try? JSONSerialization.jsonObject(with: data, options: []) + if let dictionary = json as? [String: Any] { + if let code = dictionary["code"] as? Int, code == 200 { + YXTracking.shared.success(message: "SDK push event success Response:\n\(String(data: data, encoding: .utf8) ?? "")"); + } else { + YXTrackingSQLiteManager.addTrackingData(data: YXTracking.shared.eventRequestResult) + YXTracking.shared.error(message: "SDK push event fail Response:\n\(String(data: data, encoding: .utf8) ?? "")"); + } + } + } + + task.resume() + } + + /// 推送多条日志 + public class func pushEventList() { + let count = YXTrackingSQLiteManager.getTrackingDataCount() + guard count > 0 else { return } + + let urlString = "\(YXTracking.shared.host)/api/ExternalEventlogs/AddEventListLog" + guard let url = URL(string: urlString) else { + return + } + + // 创建URL请求 + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // 读取数据库数据 + let eventList = YXTrackingSQLiteManager.readTrackingData() + var event: [EventRequestResult] = [] + for item in eventList { + event.append(item) + } + + // 将event list对象转换为JSON数据 + let jsonEncoder = JSONEncoder() + do { + let jsonData = try jsonEncoder.encode(event) + request.httpBody = jsonData + } catch { + YXTracking.shared.error(message: "SDK push event list error encoding\n\(error)"); + return + } + + // 发送请求 + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + YXTracking.shared.error(message: "SDK push event list error sending\n\(error)"); + return + } + + guard let data = data else { + return + } + + let json = try? JSONSerialization.jsonObject(with: data, options: []) + if let dictionary = json as? [String: Any] { + if let code = dictionary["code"] as? Int, code == 200 { + YXTrackingSQLiteManager.deleteTrackingData() + YXTracking.shared.success(message: "SDK push event list success Response:\n\(String(data: data, encoding: .utf8) ?? "")"); + } else { + YXTracking.shared.error(message: "SDK push event fail Response:\n\(String(data: data, encoding: .utf8) ?? "")"); + } + } + } + + task.resume() + } + + // 监听应用切换到后台 + func addEnterBackgroundNotificationObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(onPushEventList), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + // 监听系统即将终止应用程序 + func addWillTerminateNotificationObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(onPushEventList), name: UIApplication.willTerminateNotification, object: nil) + } + + // 监听回调:推送事件列表 + @objc func onPushEventList() { + YXTracking.pushEventList() + } + +} + +/// 扩展 +extension YXTracking { + + /// 日志打印 + func log(message: String) { + guard YXTracking.shared.enableLog else { return } + print("ℹ️ [YXTracking LOG] \(message)") + } + + func debug(message: String) { + guard YXTracking.shared.enableLog else { return } + print("🐛 [YXTracking DEBUG] \(message)") + } + + func warning(message: String) { + guard YXTracking.shared.enableLog else { return } + print("⚠️ [YXTracking WARNING] \(message)") + } + + func success(message: String) { + guard YXTracking.shared.enableLog else { return } + print("✅ [YXTracking SUCCESS] \(message)") + } + + func error(message: String) { + guard YXTracking.shared.enableLog else { return } + print("❌ [YXTracking ERROR] \(message)") + } + + func fatal(message: String) { + guard YXTracking.shared.enableLog else { return } + print("💀 [YXTracking FATAL] \(message)") + } + +} diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.pbxproj b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.pbxproj similarity index 65% rename from YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.pbxproj rename to YXTrackingSDK/YXTrackingSDK.xcodeproj/project.pbxproj index 3e95475..25ab89c 100644 --- a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.pbxproj +++ b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.pbxproj @@ -6,8 +6,42 @@ objectVersion = 77; objects = { +/* Begin PBXAggregateTarget section */ + 7C1AA9712F288FED00382E10 /* YXTrackingSDKAggregate */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 7C1AA9722F288FED00382E10 /* Build configuration list for PBXAggregateTarget "YXTrackingSDKAggregate" */; + buildPhases = ( + 7C1AA9772F28900800382E10 /* ShellScript */, + ); + dependencies = ( + 7C1AA9762F288FFF00382E10 /* PBXTargetDependency */, + ); + name = YXTrackingSDKAggregate; + packageProductDependencies = ( + ); + productName = YXTrackingSDKAggregate; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 7C1AA9792F2892D700382E10 /* YXTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AA9782F2892D700382E10 /* YXTracking.swift */; }; + 7C84FBDD2F35D11600A0FC52 /* YXTrackingSQLiteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C84FBDC2F35D11600A0FC52 /* YXTrackingSQLiteManager.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7C1AA9752F288FFF00382E10 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C1AA9562F288F6300382E10 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C1AA95E2F288F6300382E10; + remoteInfo = YXTrackingSDK; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 7C1AA95F2F288F6300382E10 /* YXTrackingSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = YXTrackingSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C1AA9782F2892D700382E10 /* YXTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YXTracking.swift; sourceTree = ""; }; + 7C84FBDC2F35D11600A0FC52 /* YXTrackingSQLiteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YXTrackingSQLiteManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -32,6 +66,8 @@ 7C1AA9552F288F6300382E10 = { isa = PBXGroup; children = ( + 7C1AA9782F2892D700382E10 /* YXTracking.swift */, + 7C84FBDC2F35D11600A0FC52 /* YXTrackingSQLiteManager.swift */, 7C1AA9612F288F6300382E10 /* YXTrackingSDK */, 7C1AA9602F288F6300382E10 /* Products */, ); @@ -94,6 +130,9 @@ 7C1AA95E2F288F6300382E10 = { CreatedOnToolsVersion = 26.1.1; }; + 7C1AA9712F288FED00382E10 = { + CreatedOnToolsVersion = 26.1.1; + }; }; }; buildConfigurationList = 7C1AA9592F288F6300382E10 /* Build configuration list for PBXProject "YXTrackingSDK" */; @@ -111,6 +150,7 @@ projectRoot = ""; targets = ( 7C1AA95E2F288F6300382E10 /* YXTrackingSDK */, + 7C1AA9712F288FED00382E10 /* YXTrackingSDKAggregate */, ); }; /* End PBXProject section */ @@ -125,16 +165,46 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 7C1AA9772F28900800382E10 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n#!/bin/sh\n#要build的target名\nTARGET_NAME=${PROJECT_NAME}\nif [[ $1 ]]\nthen\nTARGET_NAME=$1\nfi\nUNIVERSAL_OUTPUT_FOLDER=\"${SRCROOT}/${PROJECT_NAME}/\"\n \n#创建输出目录,并删除之前的framework文件\nmkdir -p \"${UNIVERSAL_OUTPUT_FOLDER}\"\nrm -rf \"${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework\"\n \n#分别编译模拟器和真机的Framework\nxcodebuild -target \"${TARGET_NAME}\" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" clean build\nxcodebuild -target \"${TARGET_NAME}\" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" clean build\n \n#拷贝framework到univer目录\ncp -R \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework\" \"${UNIVERSAL_OUTPUT_FOLDER}\"\n \nlipo \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}\" -remove arm64 -output \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}\"\n \n \n#合并framework,输出最终的framework到build目录\nlipo -create -output \"${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}\" \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}\" \"${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}\"\n \n#删除编译之后生成的无关的配置文件\ndir_path=\"${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/\"\nfor file in ls $dir_path\ndo\nif [[ ${file} =~ \".xcconfig\" ]]\nthen\nrm -f \"${dir_path}/${file}\"\nfi\ndone\n#判断build文件夹是否存在,存在则删除\nif [ -d \"${SRCROOT}/build\" ]\nthen\nrm -rf \"${SRCROOT}/build\"\nfi\nrm -rf \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator\" \"${BUILD_DIR}/${CONFIGURATION}-iphoneos\"\n#打开合并后的文件夹\nopen \"${UNIVERSAL_OUTPUT_FOLDER}\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 7C1AA95B2F288F6300382E10 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7C1AA9792F2892D700382E10 /* YXTracking.swift in Sources */, + 7C84FBDD2F35D11600A0FC52 /* YXTrackingSQLiteManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 7C1AA9762F288FFF00382E10 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C1AA95E2F288F6300382E10 /* YXTrackingSDK */; + targetProxy = 7C1AA9752F288FFF00382E10 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 7C1AA9662F288F6300382E10 /* Debug */ = { isa = XCBuildConfiguration; @@ -269,26 +339,36 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = Z778GC45N8; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; + GENERATE_PRELINK_OBJECT_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MACH_O_TYPE = staticlib; + MARKETING_VERSION = 1.0.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_LDFLAGS = "-ObjC"; + OTHER_LIBTOOLFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.yuanxuan.YXTrackingSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_MODULE = YES; @@ -305,26 +385,36 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = Z778GC45N8; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; + GENERATE_PRELINK_OBJECT_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MACH_O_TYPE = staticlib; + MARKETING_VERSION = 1.0.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_LDFLAGS = "-ObjC"; + OTHER_LIBTOOLFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.yuanxuan.YXTrackingSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_MODULE = YES; @@ -335,6 +425,24 @@ }; name = Release; }; + 7C1AA9732F288FED00382E10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Z778GC45N8; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 7C1AA9742F288FED00382E10 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Z778GC45N8; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -356,6 +464,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7C1AA9722F288FED00382E10 /* Build configuration list for PBXAggregateTarget "YXTrackingSDKAggregate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C1AA9732F288FED00382E10 /* Debug */, + 7C1AA9742F288FED00382E10 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 7C1AA9562F288F6300382E10 /* Project object */; diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..b90b5ed Binary files /dev/null and b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcshareddata/xcschemes/YXTrackingSDK.xcscheme b/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcshareddata/xcschemes/YXTrackingSDK.xcscheme new file mode 100644 index 0000000..1a25797 --- /dev/null +++ b/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcshareddata/xcschemes/YXTrackingSDK.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcuserdata/a1234.xcuserdatad/xcschemes/xcschememanagement.plist b/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcuserdata/a1234.xcuserdatad/xcschemes/xcschememanagement.plist similarity index 56% rename from YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcuserdata/a1234.xcuserdatad/xcschemes/xcschememanagement.plist rename to YXTrackingSDK/YXTrackingSDK.xcodeproj/xcuserdata/a1234.xcuserdatad/xcschemes/xcschememanagement.plist index c00689f..cca050e 100644 --- a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcuserdata/a1234.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/YXTrackingSDK/YXTrackingSDK.xcodeproj/xcuserdata/a1234.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,19 @@ orderHint 0 + YXTrackingSDKAggregate.xcscheme_^#shared#^_ + + orderHint + 1 + + + SuppressBuildableAutocreation + + 7C1AA95E2F288F6300382E10 + + primary + + diff --git a/YXTrackingSDK/YXTrackingSDK/YXTracking.swift b/YXTrackingSDK/YXTrackingSDK/YXTracking.swift deleted file mode 100644 index 8bb1d4b..0000000 --- a/YXTrackingSDK/YXTrackingSDK/YXTracking.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// YXTracking.swift -// YXTrackingSDK -// -// Created by 1234 on 2026/1/27. -// - -import UIKit - -class YXTracking: NSObject { - -} diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.docc/YXTrackingSDK.md b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.docc/YXTrackingSDK.md similarity index 100% rename from YXTrackingSDK/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.docc/YXTrackingSDK.md rename to YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.docc/YXTrackingSDK.md diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift new file mode 100644 index 0000000..be048e8 --- /dev/null +++ b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift @@ -0,0 +1,85 @@ +// +// YXTrackingSDK.swift +// YXTrackingSDK +// +// Created by 1234 on 2026/1/27. +// + +import Foundation + +/// 定义一个结构体来存储用户信息 +public struct UserInfo: Codable { + var userId: Int64? // 用户ID + var userName: String? // 用户名称 + var account: String? // 账号 +} + +/// 定义一个结构体来存储事件参数 +public struct EventParams: Codable { + var page: String? // 事件页面controller名称 + var buttonId: String? // 事件按钮名称 + var url: String? // 事件action +} + +/// 定义一个结构体来存储设备信息 +public struct DeviceInfo: Codable { + var os: String? // 操作系统 + var model: String? // 设备型号 + var screenResolution: String? // 屏幕分辨率 +} + +/// 定义一个结构体来存储事件请求参数 +public struct EventRequestResult: Codable { + var system_code: String? // 系统id(关联 dim_system.system_code) + var eventType: String? // 事件类型(关联 dim_event_type.event_code) + var userInfo: UserInfo? // 用户信息 + var clientType: Int32? // 数据埋点 客户端类型枚举 1、Android 2、iOS 3、PC 4、H5 + var clientTimestamp: Int64? // 事件发生时间(ISO8601格式) 毫秒时间戳 + var timestamp: String? // 事件发生时间(ISO8601格式) + var deviceInfo: DeviceInfo? // 设备信息 + var eventParams: EventParams? // 事件参数 + var customTags: Array? // 自定义标签(可选) +} + +/// 定义一个结构体来存储系统信息 +public struct SystemInfo: Codable { + var id: Int64? // ID + var createTime: String? // 创建时间 + var modifyTime: String? // 修改时间 + var system_code: String? // 系统唯一标识 + var description: String? // 系统描述 +} + +/// 定义一个结构体来存储接入系统事件类型信息 +public struct SystemEventType: Codable { + var id: Int64? // ID + var createTime: String? // 创建时间 + var modifyTime: String? // 修改时间 + var system_id: Int64? // 所属系统ID(关联dim_system.system_id) + var event_code: String? // 事件编码(如button_click、page_view) + var event_name: String? // 事件名称(如按钮点击、页面访问) + var description: String? // 事件描述 + var system_name: String? // 所属系统名称 +} + + + +/// 定义一个结构体来存储系统自定义标签信息 +public struct SystemCustonTag: Codable { + var id: Int64? // ID + var createTime: String? // 创建时间 + var modifyTime: String? // 修改时间 + var system_id: Int64? // 所属系统ID(关联dim_system.system_id) + var tag_name: String? // 标签名称(如promotion_id、source_channel) + var description: String? // 标签描述 + var is_required: Bool? // 是否为必传标签 + var system_name: String? // 所属系统 +} + +/// 定义一个结构体来存储系统所有维度信息响应结果 +public struct SystemAllDimInfoResponseResult: Codable { + var systemInfo: SystemInfo? // 接入系统 + var systemEventTypes: Array? // 接入系统事件类型 + var SystemCustonTas: Array? // 接入系统自定义标签 +} + diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 2ab321e..0000000 Binary files a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift deleted file mode 100644 index b4a628f..0000000 --- a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// YXTrackingSDK.swift -// YXTrackingSDK -// -// Created by 1234 on 2026/1/27. -// - -import Foundation - diff --git a/YXTrackingSDK/YXTrackingSQLiteManager.swift b/YXTrackingSDK/YXTrackingSQLiteManager.swift index 17e80a6..32a3c86 100644 --- a/YXTrackingSDK/YXTrackingSQLiteManager.swift +++ b/YXTrackingSDK/YXTrackingSQLiteManager.swift @@ -6,7 +6,214 @@ // import UIKit +import SQLite3 class YXTrackingSQLiteManager: NSObject { + + // 添加跟踪数据到数据库 + class func addTrackingData(data: EventRequestResult) { + // 创建数据库连接 + let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path + var db: OpaquePointer? = nil + if sqlite3_open(path, &db) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite open failed!"); + return + } + + YXTracking.shared.success(message: "SDK sqlite open success!"); + + // 创建表 + let createTableSQL = """ + CREATE TABLE IF NOT EXISTS tracking_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + system_code TEXT, + event_type TEXT, + user_info BLOB, + client_type INT, + client_timestamp INT64, + timestamp TEXT, + device_info BLOB, + event_params BLOB, + custom_tags BLOB + ) + """ + if sqlite3_exec(db, createTableSQL, nil, nil, nil) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite create table failed!"); + return + } + + YXTracking.shared.success(message: "SDK sqlite create table success!"); + + // 插入数据 + let encoder = JSONEncoder() + let insertSQL = "INSERT INTO tracking_data (system_code, event_type, user_info, client_type, client_timestamp, timestamp, device_info, event_params, custom_tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" + var statement: OpaquePointer? = nil + if sqlite3_prepare_v2(db, insertSQL, -1, &statement, nil) == SQLITE_OK { + let system_code = data.system_code ?? "" + let event_type = data.eventType ?? "" + let user_info = (data.userInfo != nil) ? try! encoder.encode(data.userInfo) : nil + let client_type = data.clientType ?? 0 + let client_timestamp = data.clientTimestamp ?? 0 + let timestamp = data.timestamp ?? "" + let device_info = try! encoder.encode(data.deviceInfo) + let event_params = try! encoder.encode(data.eventParams) + let custom_tags = try! JSONSerialization.data(withJSONObject: data.customTags ?? [], options: []) + if sqlite3_bind_text(statement, 1, system_code, -1, nil) != SQLITE_OK || sqlite3_bind_text(statement, 2, event_type, -1, nil) != SQLITE_OK || ((data.userInfo != nil) ? (sqlite3_bind_blob(statement, 3, user_info! as CFTypeRef as? UnsafeRawPointer, Int32(user_info!.count), nil) != SQLITE_OK) : (sqlite3_bind_null(statement, 3) != SQLITE_OK)) || sqlite3_bind_int(statement, 4, client_type) != SQLITE_OK || sqlite3_bind_int64(statement, 5, client_timestamp) != SQLITE_OK || sqlite3_bind_text(statement, 6, timestamp, -1, nil) != SQLITE_OK || sqlite3_bind_blob(statement, 7, device_info as CFTypeRef as? UnsafeRawPointer, Int32(device_info.count), nil) != SQLITE_OK || sqlite3_bind_blob(statement, 8, event_params as CFTypeRef as? UnsafeRawPointer, Int32(event_params.count), nil) != SQLITE_OK || sqlite3_bind_blob(statement, 9, custom_tags as CFTypeRef as? UnsafeRawPointer, Int32(custom_tags.count), nil) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite bind parameter failed!"); + return + } else if sqlite3_step(statement) != SQLITE_DONE { + YXTracking.shared.error(message: "SDK sqlite insert data failed!"); + return + } + + YXTracking.shared.success(message: "SDK sqlite bind parameter success!"); + } else { + YXTracking.shared.error(message: "SDK sqlite preparing to insert data failed!"); + return + } + + YXTracking.shared.success(message: "SDK sqlite insert data success!"); + + // 关闭数据库连接 + do { + sqlite3_close(db) + } + + } + + // 读取跟踪数据 + class func readTrackingData() -> [EventRequestResult] { + var trackingData: [EventRequestResult] = [] + // 创建数据库连接 + let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path + var db: OpaquePointer? = nil + if sqlite3_open(path, &db) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite open failed!"); + return trackingData + } + + // 查询数据 + let decoder = JSONDecoder() + let querySQL = "SELECT system_code, event_type, user_info, client_type, client_timestamp, timestamp, device_info, event_params, custom_tags FROM tracking_data" + var statement: OpaquePointer? = nil + if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK { + while (sqlite3_step(statement) == SQLITE_ROW) { + // 获取数据并转换为EventRequestResult + let system_code = String(cString: sqlite3_column_text(statement, 1)) + let event_type = String(cString: sqlite3_column_text(statement, 2)) + var user_info = UserInfo(userId: nil, userName: nil, account: nil) + if sqlite3_column_type(statement, 3) != SQLITE_NULL { + if let user_info_blob = sqlite3_column_blob(statement, 3) { + let user_info_blob_length = sqlite3_column_bytes(statement, 3) + let data = Data(bytes: user_info_blob, count: Int(user_info_blob_length)) + user_info = try! decoder.decode(UserInfo.self, from: data) + } + } + let client_type = sqlite3_column_int(statement, 4) + let client_timestamp = sqlite3_column_int64(statement, 5) + let timestamp = String(cString: sqlite3_column_text(statement, 6)) + var device_info = DeviceInfo(os: nil, model: nil, screenResolution: nil) + if sqlite3_column_type(statement, 7) != SQLITE_NULL { + if let device_info_blob = sqlite3_column_blob(statement, 7) { + let device_info_blob_length = sqlite3_column_bytes(statement, 7) + let data = Data(bytes: device_info_blob, count: Int(device_info_blob_length)) + device_info = try! decoder.decode(DeviceInfo.self, from: data) + } + } + var event_params = EventParams(page: nil, buttonId: nil, url: nil) + if sqlite3_column_type(statement, 8) != SQLITE_NULL { + if let event_params_blob = sqlite3_column_blob(statement, 8) { + let event_params_blob_length = sqlite3_column_bytes(statement, 8) + let data = Data(bytes: event_params_blob, count: Int(event_params_blob_length)) + event_params = try! decoder.decode(EventParams.self, from: data) + } + } + var custom_tags = Array() + if sqlite3_column_type(statement, 9) != SQLITE_NULL { + if let custom_tags_blob = sqlite3_column_blob(statement, 9) { + let custom_tags_blob_length = sqlite3_column_bytes(statement, 9) + let data = Data(bytes: custom_tags_blob, count: Int(custom_tags_blob_length)) + custom_tags = try! decoder.decode([String].self, from: data) + } + } + + let event = EventRequestResult(system_code: system_code, + eventType: event_type, + userInfo: user_info, + clientType: client_type, + clientTimestamp: client_timestamp, + timestamp: timestamp, + deviceInfo: device_info, + eventParams: event_params, + customTags: custom_tags) + trackingData.append(event) + } + sqlite3_finalize(statement) + } else { + YXTracking.shared.error(message: "SDK sqlite read data failed!"); + } + + // 关闭数据库连接 + do { + sqlite3_close(db) + } + return trackingData + } + + // 删除跟踪数据 + class func deleteTrackingData() { + // 创建数据库连接 + let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path + var db: OpaquePointer? = nil + if sqlite3_open(path, &db) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite open failed!"); + return + } + + // 删除数据 + let deleteSQL = "DELETE FROM tracking_data" + if sqlite3_exec(db, deleteSQL, nil, nil, nil) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite delete data failed!"); + } + + // 关闭数据库连接 + do { + sqlite3_close(db) + } + } + + // 获取数据库缓存条数 + class func getTrackingDataCount() -> Int { + // 创建数据库连接 + let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path + var db: OpaquePointer? = nil + if sqlite3_open(path, &db) != SQLITE_OK { + YXTracking.shared.error(message: "SDK sqlite open failed!"); + return -1 + } + + // 查询数据 + var dataCount = -1 + let querySQL = "SELECT COUNT(*) FROM tracking_data" + var statement: OpaquePointer? = nil + if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK { + sqlite3_step(statement) + dataCount = Int(sqlite3_column_int64(statement, 0)) + sqlite3_finalize(statement) + } else { + YXTracking.shared.error(message: "SDK sqlite read data failed!"); + } + + // 关闭数据库连接 + do { + sqlite3_close(db) + } + + return dataCount + } }