接入文档:https://inspire.sg.larksuite.com/docx/RPgndd77VofvNBxPcQ8lBMJngeb 渠道号: ![[Pasted image 20250814091956.png]] hj和ds的生产环境配置: ``` 测试环境:http://192.168.1.33:8080/platform_new/ 正式环境:https://developer.ilnc.inspiregames.cn:8888/platform/ hj 123456 ds 123456 正式环境 用户信息 X项目: junbao@inspiregames.cn 244466666 元素: aide@inspiregames.cn Yuan1234 正式环境 上报地址: 元素:ilnc.icongamesg.com X项目:ilnc.doomsurvivor.com ``` 参考接入逻辑: ## FirebaseMessageUtil ``` cs using Firebase.Extensions; using Ideatech; using LuaInterface; using Newtonsoft.Json; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Networking; /// /// FCM推送相关 /// public class FirebaseMessageUtil { private static FirebaseMessageUtil _instance; public bool isSuccess = false;//初始化是否成功的标志,在FirebaseUtils脚本中同步该标志位 private string fcmAppid_PT; //Application ID of fcm for PTServer private string fcmAppid_MX; //Application ID of fcm for MXServer private string fcmAppid_Test; //Application ID of fcm for TestServer public string fcmToken; //token of fcm private FirebaseMessageUtil() { } public static FirebaseMessageUtil Instance { get { if (_instance == null) { _instance = new FirebaseMessageUtil(); } return _instance; } } [NoToLua] public void InitializeFirebase() { Firebase.Messaging.FirebaseMessaging.MessageReceived += OnMessageReceived; Firebase.Messaging.FirebaseMessaging.TokenReceived += OnTokenReceived; if (!ThirdPartyConst.TryGetValue("fcmAppidpt", out fcmAppid_PT)) { Debug.LogWarning("Failed to obtain the FCM application ID: fcmAppidpt"); } if (!ThirdPartyConst.TryGetValue("fcmAppidmx", out fcmAppid_MX)) { Debug.LogWarning("Failed to obtain the FCM application ID: fcmAppidmx"); } if (!ThirdPartyConst.TryGetValue("fcmAppidtest", out fcmAppid_Test)) { Debug.LogWarning("Failed to obtain the FCM application ID: fcmAppidtest"); } } [NoToLua] public void RemoveFirebase() { Firebase.Messaging.FirebaseMessaging.MessageReceived -= OnMessageReceived; Firebase.Messaging.FirebaseMessaging.TokenReceived -= OnTokenReceived; } private void OnMessageReceived(object sender, Firebase.Messaging.MessageReceivedEventArgs e) { Dictionary args = new Dictionary(); args.Add(NativeEventConst.EVENT_ID_KEY, NativeEventConst.FIREBASE_MESSAGE_RECEIVED); var notification = e.Message.Notification; if (notification != null) { args.Add("title", notification.Title); args.Add("body", notification.Body); var android = notification.Android; if (android != null) { args.Add("android channel_id", android.ChannelId); } } if (e.Message.From.Length > 0) { args.Add("from", e.Message.From); } if (e.Message.Link != null) { args.Add("link", e.Message.Link.ToString()); } if (e.Message.Data.Count > 0) { foreach (KeyValuePair item in e.Message.Data) { args.Add(item.Key, item.Value); } } //NativeUtils.CallBackToLua(args); } private void OnTokenReceived(object sender, Firebase.Messaging.TokenReceivedEventArgs token) { fcmToken = token.Token; Debug.Log("Received FCM Registration Token: " + token.Token); } /// /// 检测json合法性 /// /// /// private (bool isValid, string error) CheckJsonDataValid(Dictionary jsonMap) { if (jsonMap == null) return (false, "The dictionary object is null"); var emptyItems = jsonMap .Where(kv => IsInvalidValue(kv.Value)) .Select(kv => kv.Key.ToString()) .ToList(); return emptyItems.Count == 0 ? (true, null) : (false, $"The following key values are empty: {string.Join(", ", emptyItems)}"); } private bool IsInvalidValue(T value) { if (value == null) return true; if (value is string str) return string.IsNullOrEmpty(str); return false; } /// /// 供Lua层调用,设备上报 /// public void EquipmentPost(string url, string jsonData) { if (isSuccess) { //检测json键值合法性 Dictionary paramsMap = JsonConvert.DeserializeObject>(jsonData); var (isValid, error) = CheckJsonDataValid(paramsMap); if (!isValid) { Debug.LogWarning($"Equipment reporting failed: {error}"); return; } CoroutineManager.AddCoroutine(PostRequest(url, jsonData)); } else { Debug.LogWarning("The device failed to report and the SDK was not successfully initialized"); } } [System.Serializable] private class PostResponse { public int code; public string message; public string data; } private IEnumerator PostRequest(string url, string jsonData) { using (UnityWebRequest request = UnityWebRequest.Post(url, "POST")) { byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonData); request.uploadHandler = new UploadHandlerRaw(bodyRaw); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogWarning("Equipment reporting failed." + request.error); yield break; } var response = JsonUtility.FromJson(request.downloadHandler.text); if (response.code != 0) { Debug.LogWarning("The device failed to report the error code" + response.code); } }; } /// /// 供Lua调用,订阅消息主题,订阅成功后可以通过主题推送 /// /// public void SubscribeTopic(string topicJsonStr) { if (!isSuccess) { Debug.LogWarning("Subscription to the topic failed! The SDK was not initialized successfully"); return; } //检测json键值合法性 Dictionary paramsMap = JsonConvert.DeserializeObject>(topicJsonStr); var (isValid, error) = CheckJsonDataValid(paramsMap); if (!isValid) { Debug.LogWarning($"Failed to subscribe to the topic: {error}"); return; } foreach (string key in paramsMap.Keys) { Firebase.Messaging.FirebaseMessaging.SubscribeAsync(paramsMap[key]).ContinueWithOnMainThread(task => { if (task.IsFaulted) Debug.LogWarning($"Failed to subscribe to the topic: {task.Exception}"); }); } } /// /// 供Lua调用,根据服务器类型,获取应用ID /// /// /// public string GetAppIdByServer(string serverCode) { if (string.IsNullOrEmpty(serverCode)) { return fcmAppid_Test; } else { string lowerServerCode = serverCode.ToLower(); if (lowerServerCode == "pt") { return fcmAppid_PT; } else if (lowerServerCode == "es") { return fcmAppid_MX; } else { return ""; } } } } ``` ## ios消息通知扩展: 参考链接: https://firebase.google.com/docs/cloud-messaging/ios/send-image?hl=zh-cn#node.js https://blog.csdn.net/qq_38718912/article/details/126975533 FCM带图片,发送给客户端回包,数据结构: ``` json FCM: userInfo: { aps = { alert = { body = 2; title = 1; }; "mutable-content" = 1; }; "fcm_options" = { image = "https://octodex.github.com/images/codercat.jpg"; }; "gcm.message_id" = 1755861458675849; "google.c.a.e" = 1; "google.c.fid" = cRZAyUCe5Uxcihd2iGk7Yf; "google.c.sender.id" = 967184518610; traceId = "D0bWCJoRYhib3+FPKvN8ueLz6EX+ITcDBifBlie+Ki6iMKwlC4h4dVYQoWGC845E"; }. FCM: userInfo: { aps = { alert = { body = 2; title = 1; }; "mutable-content" = 1; }; "fcm_options" = { image = "https://octodex.github.com/images/codercat.jpg"; }; "gcm.message_id" = 1755861458675849; "google.c.a.e" = 1; "google.c.fid" = cRZAyUCe5Uxcihd2iGk7Yf; "google.c.sender.id" = 967184518610; traceId = "D0bWCJoRYhib3+FPKvN8ueLz6EX+ITcDBifBlie+Ki6iMKwlC4h4dVYQoWGC845E"; }. ``` 如有需要,还要在podFile里面,添加fcm相关的插件,到消息通知扩展插件 ``` target 'NotificationService' do pod 'Firebase/Messaging', '12.0.0' end ``` 编辑Editor脚本: ## NotificationServiceExtensionCreator.cs ``` cs #if UNITY_IOS using System.IO; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; using UnityEditor.iOS.Xcode.Extensions; using UnityEngine; using UnityEditor.iOS.Xcode.PBX; public static class PureNotificationExtensionCreator { private const string ExtensionName = "NotificationService"; [PostProcessBuild(200)] public static void OnPostProcessBuild(BuildTarget target, string buildPath) { if (target != BuildTarget.iOS) return; Debug.Log("🛠️ 开始创建纯净版 Notification Service Extension..."); string projPath = PBXProject.GetPBXProjectPath(buildPath); PBXProject proj = new PBXProject(); proj.ReadFromFile(projPath); #if UNITY_2019_3_OR_NEWER string mainTargetGuid = proj.GetUnityMainTargetGuid(); #else string mainTargetGuid = proj.TargetGuidByName(PBXProject.GetUnityTargetName()); #endif // 1. 创建 Extension Target(这会自动创建基础文件) string extGuid = proj.AddAppExtension(mainTargetGuid, ExtensionName, ExtensionName, $"{ExtensionName}/Info.plist"); Debug.Log($"✅ 创建 Extension Target: {extGuid}"); // 2. 配置基本设置 proj.SetBuildProperty(extGuid, "PRODUCT_BUNDLE_IDENTIFIER", PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.iOS) + "." + ExtensionName); proj.SetBuildProperty(extGuid, "ENABLE_BITCODE", "NO"); proj.SetBuildProperty(extGuid, "IPHONEOS_DEPLOYMENT_TARGET", "15.0"); // 3. 配置签名 ConfigureCodeSigning(proj, extGuid); // 4. 移动预制的代码文件到扩展目录 MovePrebuiltSourceFiles(buildPath); // 5. 添加系统框架 proj.AddFrameworkToProject(extGuid, "UserNotifications.framework", false); // 6. 配置 Push Notifications 能力 ConfigurePushNotificationsCapability(proj, extGuid, buildPath); // 7. 关键修复:确保文件被添加到 Xcode 项目结构和编译阶段 AddFilesToProjectStructure(proj, extGuid, buildPath); // 8. 保存工程 proj.WriteToFile(projPath); AssetDatabase.Refresh(); Debug.Log("🎉 纯净版 Notification Service Extension 创建完成!"); } // 关键方法:确保文件被添加到 Xcode 项目结构 private static void AddFilesToProjectStructure(PBXProject proj, string extGuid, string buildPath) { Debug.Log("🔧 开始添加文件到 Xcode 项目结构..."); // 1. 获取或创建 Sources Build Phase var buildPhaseID = proj.AddSourcesBuildPhase(extGuid); Debug.Log($"✅ Sources Build Phase ID: {buildPhaseID}"); // 2. 添加 .h 文件到项目结构 string headerPath = Path.Combine(ExtensionName, "NotificationService.h"); string headerGuid = proj.AddFile(headerPath, headerPath, PBXSourceTree.Source); Debug.Log($"✅ Header 文件添加到项目: {headerGuid}"); // 3. 添加 .m 文件到项目结构 string sourcePath = Path.Combine(ExtensionName, "NotificationService.m"); string sourceGuid = proj.AddFile(sourcePath, sourcePath, PBXSourceTree.Source); Debug.Log($"✅ Source 文件添加到项目: {sourceGuid}"); // 4. 将文件添加到编译阶段(确保 Target Membership 正确) proj.AddFileToBuildSection(extGuid, buildPhaseID, headerGuid); proj.AddFileToBuildSection(extGuid, buildPhaseID, sourceGuid); Debug.Log($"✅ 文件已添加到编译阶段"); // 5. 确保 Info.plist 也被添加到项目 string plistPath = Path.Combine(ExtensionName, "Info.plist"); string plistGuid = proj.AddFile(plistPath, plistPath, PBXSourceTree.Source); Debug.Log($"✅ Info.plist 添加到项目: {plistGuid}"); Debug.Log("🎯 所有文件已成功添加到 Xcode 项目结构!"); } private static void ConfigureCodeSigning(PBXProject proj, string extGuid) { string teamId = PlayerSettings.iOS.appleDeveloperTeamID; if (!string.IsNullOrEmpty(teamId)) { proj.SetBuildProperty(extGuid, "DEVELOPMENT_TEAM", teamId); proj.SetBuildProperty(extGuid, "CODE_SIGN_STYLE", "Automatic"); } } private static void MovePrebuiltSourceFiles(string buildPath) { string extDir = Path.Combine(buildPath, ExtensionName); // 确保目录存在 if (!Directory.Exists(extDir)) Directory.CreateDirectory(extDir); // 源文件路径(Plugins/iOS 目录) string sourceDir = Path.Combine(Application.dataPath, "Plugins", "iOS"); // 移动 .h 文件 string headerSource = Path.Combine(sourceDir, "NotificationService.h"); string headerDest = Path.Combine(extDir, "NotificationService.h"); if (File.Exists(headerSource)) { // 如果目标文件已存在,先删除 if (File.Exists(headerDest)) { File.Delete(headerDest); Debug.Log("🗑️ 删除已存在的目标文件: " + headerDest); } File.Move(headerSource, headerDest); Debug.Log("✅ 移动 NotificationService.h 文件"); } else { Debug.LogError("❌ 找不到源文件: " + headerSource); } // 移动 .m 文件 string sourceSource = Path.Combine(sourceDir, "NotificationService.m"); string sourceDest = Path.Combine(extDir, "NotificationService.m"); if (File.Exists(sourceSource)) { // 如果目标文件已存在,先删除 if (File.Exists(sourceDest)) { File.Delete(sourceDest); Debug.Log("🗑️ 删除已存在的目标文件: " + sourceDest); } File.Move(sourceSource, sourceDest); Debug.Log("✅ 移动 NotificationService.m 文件"); } else { Debug.LogError("❌ 找不到源文件: " + sourceSource); } // 创建 Info.plist(如果需要) string plistPath = Path.Combine(extDir, "Info.plist"); if (!File.Exists(plistPath)) { File.WriteAllText(plistPath, PureInfoPlistContent); Debug.Log("✅ 创建 Info.plist 文件"); } } private static void ConfigurePushNotificationsCapability(PBXProject proj, string extGuid, string buildPath) { Debug.Log("🔧 开始配置 Push Notifications 能力..."); // 1. 添加 Push Notifications 能力 proj.AddCapability(extGuid, PBXCapabilityType.PushNotifications); Debug.Log("✅ 添加 Push Notifications 能力"); // 2. 确保签名配置正确 string teamId = PlayerSettings.iOS.appleDeveloperTeamID; if (!string.IsNullOrEmpty(teamId)) { proj.SetBuildProperty(extGuid, "DEVELOPMENT_TEAM", teamId); proj.SetBuildProperty(extGuid, "CODE_SIGN_STYLE", "Automatic"); Debug.Log($"✅ 配置开发团队: {teamId}"); } Debug.Log("✅ Push Notifications 能力配置完成"); } // 纯净的 Info.plist 内容 private static readonly string PureInfoPlistContent = @" CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName NotificationService CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString 1.0 CFBundleVersion 1 NSExtension NSExtensionPointIdentifier com.apple.usernotifications.service NSExtensionPrincipalClass NotificationService "; } #endif ``` ## NotificationService.h ``` cpp // // NotificationService.h // NotificationService // // Created by unity on 2025/8/23. // #import @interface NotificationService : UNNotificationServiceExtension @end ``` ## NotificationService.m ``` cpp // // NotificationService.m // NotificationService // // Created by unity on 2025/8/23. // #import "NotificationService.h" #import @interface NotificationService () @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; @end @implementation NotificationService - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { NSLog(@"✅ Extension被调用!"); NSLog(@"📦 原始通知内容: %@", request.content.userInfo); self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title]; // 1. 从userInfo中获取图片URL NSDictionary *userInfo = request.content.userInfo; NSString *imageUrlString = nil; // 检查FCM格式的图片URL (fcm_options -> image) if (userInfo[@"fcm_options"] && userInfo[@"fcm_options"][@"image"]) { imageUrlString = userInfo[@"fcm_options"][@"image"]; NSLog(@"🖼️ 找到FCM图片URL: %@", imageUrlString); } // 也可以检查其他自定义字段 else if (userInfo[@"image_url"]) { imageUrlString = userInfo[@"image_url"]; NSLog(@"🖼️ 找到自定义图片URL: %@", imageUrlString); } // 2. 如果没有图片URL,直接返回原始内容 if (!imageUrlString) { NSLog(@"⚠️ 未找到图片URL,使用原始内容"); self.contentHandler(self.bestAttemptContent); return; } // 3. 下载图片并添加到通知 [self loadAttachmentForUrlString:imageUrlString completionHandler:^(UNNotificationAttachment *attachment) { if (attachment) { NSLog(@"✅ 图片下载成功,添加到通知"); self.bestAttemptContent.attachments = @[attachment]; } else { NSLog(@"❌ 图片下载失败,使用原始内容"); } // 4. 最终返回通知内容 self.contentHandler(self.bestAttemptContent); }]; } - (void)loadAttachmentForUrlString:(NSString *)urlString completionHandler:(void (^)(UNNotificationAttachment *))completionHandler { __block UNNotificationAttachment *attachment = nil; NSURL *attachmentURL = [NSURL URLWithString:urlString]; if (!attachmentURL) { completionHandler(nil); return; } NSLog(@"🌐 开始下载图片: %@", urlString); NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDownloadTask *task = [session downloadTaskWithURL:attachmentURL completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) { // 检查下载错误 if (error) { NSLog(@"❌ 图片下载错误: %@", error); completionHandler(nil); return; } // 检查文件是否存在 if (!temporaryFileLocation) { NSLog(@"❌ 临时文件位置为空"); completionHandler(nil); return; } // 获取文件扩展名 NSString *fileExtension = [self getFileExtensionForResponse:response] ?: @"jpg"; NSLog(@"📄 文件扩展名: %@", fileExtension); // 创建唯一文件名 NSString *uniqueFileName = [NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString], fileExtension]; NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:uniqueFileName]; // 移动文件到临时目录 NSError *moveError = nil; [[NSFileManager defaultManager] moveItemAtURL:temporaryFileLocation toURL:[NSURL fileURLWithPath:tempFile] error:&moveError]; if (moveError) { NSLog(@"❌ 移动文件错误: %@", moveError); completionHandler(nil); return; } // 创建通知附件 attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:[NSURL fileURLWithPath:tempFile] options:nil error:&moveError]; if (moveError) { NSLog(@"❌ 创建附件错误: %@", moveError); completionHandler(nil); return; } completionHandler(attachment); }]; [task resume]; } - (NSString *)getFileExtensionForResponse:(NSURLResponse *)response { if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSString *contentType = httpResponse.allHeaderFields[@"Content-Type"]; if ([contentType isEqualToString:@"image/jpeg"]) return @"jpg"; if ([contentType isEqualToString:@"image/jpg"]) return @"jpg"; if ([contentType isEqualToString:@"image/png"]) return @"png"; if ([contentType isEqualToString:@"image/gif"]) return @"gif"; if ([contentType isEqualToString:@"image/webp"]) return @"webp"; } // 从URL路径推断扩展名 NSString *pathExtension = response.URL.pathExtension; if (pathExtension.length > 0) { return pathExtension; } return @"jpg"; // 默认使用jpg } - (void)serviceExtensionTimeWillExpire { NSLog(@"⏰ Extension处理超时,使用原始内容"); self.contentHandler(self.bestAttemptContent); } @end ```