灵光系统 临时记录_第一章.md 24 KB

接入文档: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

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;

/// <summary>
/// FCM推送相关
/// </summary>
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<string, object> args = new Dictionary<string, object>();
        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<string, string> 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);
    }

    /// <summary>
    /// 检测json合法性
    /// </summary>
    /// <param name="jsonStr"></param>
    /// <returns></returns>
    private (bool isValid, string error) CheckJsonDataValid<TKey, TValue>(Dictionary<TKey, TValue> 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>(T value)
    {
        if (value == null) return true;
        if (value is string str) return string.IsNullOrEmpty(str);
        return false;
    }

    /// <summary>
    /// 供Lua层调用,设备上报
    /// </summary>
    public void EquipmentPost(string url, string jsonData)
    {
        if (isSuccess)
        {
            //检测json键值合法性
            Dictionary<string, string> paramsMap = JsonConvert.DeserializeObject<Dictionary<string, string>>(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<PostResponse>(request.downloadHandler.text);
            if (response.code != 0)
            {
                Debug.LogWarning("The device failed to report the error code" + response.code);
            }
        };
    }

    /// <summary>
    /// 供Lua调用,订阅消息主题,订阅成功后可以通过主题推送
    /// </summary>
    /// <param name="topic"></param>
    public void SubscribeTopic(string topicJsonStr)
    {
        if (!isSuccess)
        {
            Debug.LogWarning("Subscription to the topic failed! The SDK was not initialized successfully");
            return;
        }

        //检测json键值合法性
        Dictionary<string, string> paramsMap = JsonConvert.DeserializeObject<Dictionary<string, string>>(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}");
            });
        }
    }

    /// <summary>
    /// 供Lua调用,根据服务器类型,获取应用ID
    /// </summary>
    /// <param name="serverCode"></param>
    /// <returns></returns>
    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带图片,发送给客户端回包,数据结构:

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

#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 =
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">
<plist version=""1.0"">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleDisplayName</key>
    <string>NotificationService</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key>
    <string>XPC!</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>NSExtension</key>
    <dict>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.usernotifications.service</string>
        <key>NSExtensionPrincipalClass</key>
        <string>NotificationService</string>
    </dict>
</dict>
</plist>";
}

#endif

NotificationService.h

//
//  NotificationService.h
//  NotificationService
//
//  Created by unity on 2025/8/23.
//

#import <UserNotifications/UserNotifications.h>

@interface NotificationService : UNNotificationServiceExtension

@end

NotificationService.m

//
//  NotificationService.m
//  NotificationService
//
//  Created by unity on 2025/8/23.
//

#import "NotificationService.h"
#import <UIKit/UIKit.h>

@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