修正命名错误
xiyunfei authored at 2023-04-09 21:10:58
6.46 KiB
NewLife.JT808
using NewLife.Caching;
using NewLife.Log;
using NewLife.Net;

namespace NewLife.JT808.Protocols;

/// <summary>会话管理器,维护终端手机号到网络会话的映射</summary>
/// <remarks>
/// 支持本地指令下发和跨服务器指令广播。
/// 当终端设备不在当前服务器时,通过 <see cref="CommandReceived"/> 事件通知上层,
/// 由上层(如 Stardust 事件总线)将指令广播到集群中其他服务器。
/// 
/// 使用示例:
/// <code>
/// var mgr = new SessionManager();
/// 
/// // 本地下发
/// mgr.Send("13800138000", new T8300 { ... });
/// 
/// // 跨服务器广播(需上层接入星尘事件总线)
/// mgr.CommandReceived += (s, e) => {
///     var evt = new CommandEvent { Mobile = e.Mobile, ... };
///     eventBus.PublishAsync(evt);
/// };
/// </code>
/// </remarks>
public class SessionManager : ICommandBus
{
    #region 属性
    private readonly ICache _cache;

    /// <summary>指令日志</summary>
    public ILog Log { get; set; } = Logger.Null;

    /// <summary>当本地未找到目标设备时触发,由上层负责广播到远程服务器</summary>
    public event EventHandler<CommandEventArgs>? CommandReceived;
    #endregion

    #region 构造
    /// <summary>实例化会话管理器</summary>
    public SessionManager()
    {
        _cache = new MemoryCache { Period = 60, Expire = 1200 };
    }
    #endregion

    #region 会话管理
    /// <summary>注册会话。终端上线时调用</summary>
    /// <param name="mobile">终端手机号</param>
    /// <param name="session">网络会话</param>
    public void Register(String mobile, INetSession session)
    {
        if (mobile.IsNullOrEmpty()) return;

        _cache.Set(mobile, session, 1200);

        Log?.Info("会话注册:Mobile={0}, Session={1}", mobile, session);
    }

    /// <summary>移除会话。终端离线时调用</summary>
    /// <param name="mobile">终端手机号</param>
    public void Remove(String mobile)
    {
        if (mobile.IsNullOrEmpty()) return;

        _cache.Remove(mobile);

        Log?.Info("会话移除:Mobile={0}", mobile);
    }

    /// <summary>根据手机号查找对应的网络会话</summary>
    /// <param name="mobile">终端手机号</param>
    /// <returns>网络会话,未找到返回null</returns>
    public INetSession? Find(String mobile)
    {
        if (mobile.IsNullOrEmpty()) return null;

        _cache.TryGetValue<INetSession>(mobile, out var session);
        return session;
    }

    /// <summary>判断终端是否在线(连接到本服务器)</summary>
    /// <param name="mobile">终端手机号</param>
    /// <returns>是否在线</returns>
    public Boolean IsOnline(String mobile) => Find(mobile) != null;

    /// <summary>获取所有在线会话的列表</summary>
    /// <returns>在线会话列表</returns>
    public IDictionary<String, INetSession> GetAll()
    {
        var keys = _cache.Keys;
        if (keys == null || keys.Count == 0) return new Dictionary<String, INetSession>();

        return _cache.GetAll<INetSession>(keys);
    }
    #endregion

    #region ICommandBus 实现
    /// <summary>向指定终端发送指令,不等待响应</summary>
    /// <remarks>
    /// 优先查找本地会话,找到则直接下发;
    /// 未找到时触发 <see cref="CommandReceived"/> 事件,由上层广播到远程服务器。
    /// </remarks>
    /// <param name="mobile">终端手机号</param>
    /// <param name="body">消息体对象</param>
    /// <returns>本地发送成功或在集群中广播成功返回 true</returns>
    public Boolean Send(String mobile, Object body)
    {
        // 优先本地查找
        var session = Find(mobile);
        if (session is ICommand cmd)
        {
            cmd.Send(body);
            Log?.Info("指令本地下发:Mobile={0}, Body={1}", mobile, body);
            return true;
        }

        // 本地未找到,触发远程广播事件
        var args = new CommandEventArgs(mobile, body);
        CommandReceived?.Invoke(this, args);

        if (args.Handled)
        {
            Log?.Info("指令远程广播:Mobile={0}, Body={1}", mobile, body);
            return true;
        }

        Log?.Warn("指令下发失败,设备不在线:Mobile={0}", mobile);
        return false;
    }

    /// <summary>向指定终端发送指令并等待响应</summary>
    /// <typeparam name="TResult">响应消息体类型</typeparam>
    /// <param name="mobile">终端手机号</param>
    /// <param name="body">请求消息体</param>
    /// <returns>响应消息体,超时或失败返回 null</returns>
    public async Task<TResult?> SendAsync<TResult>(String mobile, Object body) where TResult : class, new()
    {
        var session = Find(mobile);
        if (session is ICommand cmd)
        {
            try
            {
                var result = await cmd.SendAsync<TResult>(body);
                Log?.Info("指令本地下发(异步):Mobile={0}, Body={1}", mobile, body);
                return result;
            }
            catch (Exception ex)
            {
                Log?.Error("指令本地下发异常:Mobile={0}", mobile, ex);
                return null;
            }
        }

        // 异步下发也支持远程广播(但不等待响应)
        var args = new CommandEventArgs(mobile, body);
        CommandReceived?.Invoke(this, args);

        if (args.Handled)
        {
            Log?.Info("指令远程广播(异步):Mobile={0}, Body={1}", mobile, body);
        }
        else
        {
            Log?.Warn("指令下发失败,设备不在线:Mobile={0}", mobile);
        }

        return null;
    }
    #endregion

    #region 远程指令处理
    /// <summary>处理来自远程服务器的指令下发事件</summary>
    /// <remarks>
    /// 由事件总线处理器调用,将远程广播的指令在本服务器上执行。
    /// </remarks>
    /// <param name="mobile">目标终端手机号</param>
    /// <param name="body">消息体对象</param>
    /// <returns>是否成功发送给本地终端</returns>
    public Boolean ProcessRemoteCommand(String mobile, Object body)
    {
        var session = Find(mobile);
        if (session is ICommand cmd)
        {
            cmd.Send(body);
            Log?.Info("远程指令本地执行:Mobile={0}, Body={1}", mobile, body);
            return true;
        }

        Log?.Warn("远程指令本地执行失败,设备已离线:Mobile={0}", mobile);
        return false;
    }
    #endregion
}