cancellationToken
智能大石头 authored at 2025-09-01 23:28:59
9.37 KiB
NewLife.Remoting
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NewLife.Log;
using NewLife.Remoting.Extensions.Services;
using NewLife.Remoting.Models;
using NewLife.Remoting.Services;
using NewLife.Security;
using WebSocket = System.Net.WebSockets.WebSocket;

namespace NewLife.Remoting.Extensions;

/// <summary>设备类控制器基类</summary>
[ApiFilter]
[ApiController]
[Route("[controller]")]
public abstract class BaseDeviceController : BaseController
{
    private readonly IDeviceService _deviceService;
    private readonly ITokenService _tokenService;
    private readonly ISessionManager _sessionManager;
    private readonly ITracer _tracer;

    #region 构造
    /// <summary>实例化设备控制器</summary>
    /// <param name="serviceProvider"></param>
    public BaseDeviceController(IServiceProvider serviceProvider) : base(serviceProvider)
    {
        _deviceService = serviceProvider.GetRequiredService<IDeviceService>();
        _tokenService = serviceProvider.GetRequiredService<ITokenService>();
        _sessionManager = serviceProvider.GetRequiredService<ISessionManager>();
        _tracer = serviceProvider.GetRequiredService<ITracer>();
    }

    /// <summary>实例化设备控制器</summary>
    /// <param name="deviceService"></param>
    /// <param name="tokenService"></param>
    /// <param name="sessionManager"></param>
    /// <param name="serviceProvider"></param>
    public BaseDeviceController(IDeviceService? deviceService, ITokenService? tokenService, ISessionManager? sessionManager, IServiceProvider serviceProvider) : base(deviceService, tokenService, serviceProvider)
    {
        _deviceService = deviceService ?? serviceProvider.GetRequiredService<IDeviceService>();
        _tokenService = tokenService ?? serviceProvider.GetRequiredService<ITokenService>();
        _sessionManager = sessionManager ?? serviceProvider.GetRequiredService<ISessionManager>();
        _tracer = serviceProvider.GetRequiredService<ITracer>();
    }

    /// <summary>验证身份</summary>
    /// <param name="token"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    /// <exception cref="ApiException"></exception>
    protected override Boolean OnAuthorize(String token, DeviceContext context)
    {
        // 先调用基类,获取Jwt。即使失败,也要继续往下走,获取设备信息。最后再决定是否抛出异常
        Exception? error = null;
        try
        {
            if (!base.OnAuthorize(token, context)) return false;
        }
        catch (Exception ex)
        {
            error = ex;
        }

        var ds2 = _deviceService as IDeviceService2;

        if (context.Device == null)
        {
            var code = Jwt?.Subject;
            if (code.IsNullOrEmpty()) return false;

            var dv = ds2 != null ? ds2.GetDevice(code) : _deviceService.QueryDevice(code);
            if (dv == null || !dv.Enable) error ??= new ApiException(ApiCode.Forbidden, "无效客户端!");

            context.Device = dv!;
        }

        // 在线对象
        context.Online ??= ds2?.GetOnline(context);

        if (error != null) throw error;

        return true;
    }
    #endregion

    #region 登录注销
    /// <summary>设备登录</summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [AllowAnonymous]
    [HttpPost(nameof(Login))]
    public virtual ILoginResponse Login([FromBody] ILoginRequest request)
    {
        // 先查一次,后续即使登录失败,也可以写设备历史
        if (!request.Code.IsNullOrEmpty()) Context.Device = _deviceService.QueryDevice(request.Code);

        var rs = _deviceService.Login(Context, request, "Http");

        var dv = Context.Device!;
        rs ??= new LoginResponse { Name = dv.Name, };
        rs.Code = dv.Code;

        if (request is ILoginRequest2 req) rs.Time = req.Time;
        rs.ServerTime = DateTime.UtcNow.ToLong();

        // 动态注册的设备不可用时,不要发令牌,只发证书
        if (dv.Enable)
        {
            if (request.ClientId.IsNullOrEmpty()) Context.ClientId = request.ClientId = Rand.NextString(8);
            var tm = _tokenService.IssueToken(dv.Code, request.ClientId);

            rs.Token = tm.AccessToken;
            rs.Expire = tm.ExpireIn;
        }

        return rs;
    }

    /// <summary>设备注销</summary>
    /// <param name="reason">注销原因</param>
    /// <returns></returns>
    [HttpGet(nameof(Logout))]
    [HttpPost(nameof(Logout))]
    public virtual ILogoutResponse Logout(String? reason)
    {
        _deviceService.Logout(Context, reason, "Http");

        return new LogoutResponse
        {
            Name = Context.Device?.Name,
            Token = null,
        };
    }
    #endregion

    #region 心跳保活
    /// <summary>设备心跳</summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpGet(nameof(Ping))]
    [HttpPost(nameof(Ping))]
    public virtual IPingResponse Ping([FromBody] IPingRequest request)
    {
        var rs = _deviceService.Ping(Context, request);

        var device = Context.Device;
        if (device != null)
        {
            // 令牌有效期检查,10分钟内到期的令牌,颁发新令牌。
            // 这里将来由客户端提交刷新令牌,才能颁发新的访问令牌。
            var (jwt, ex) = _tokenService.DecodeToken(Context.Token!);
            if (ex == null && jwt != null && jwt.Expire < DateTime.Now.AddMinutes(10))
            {
                using var span = _tracer?.NewSpan("RefreshToken", new { device.Code, jwt.Subject });

                var tm = _tokenService.IssueToken(device.Code, jwt.Id);
                rs.Token = tm.AccessToken;
            }
        }

        return rs;
    }
    #endregion

    #region 升级更新
    /// <summary>升级检查</summary>
    /// <returns></returns>
    [HttpGet(nameof(Upgrade))]
    [HttpPost(nameof(Upgrade))]
    public virtual IUpgradeInfo Upgrade(String? channel)
    {
        if (Context.Device == null) throw new ApiException(ApiCode.Unauthorized, "未登录");

        // 基础路径
        var uri = Request.GetRawUrl().ToString();
        var p = uri.IndexOf('/', "https://".Length);
        if (p > 0) uri = uri[..p];

        var info = _deviceService.Upgrade(Context, channel);

        // 为了兼容旧版本客户端,这里必须把路径处理为绝对路径
        if (info != null && !info.Source.StartsWithIgnoreCase("http://", "https://"))
        {
            info.Source = new Uri(new Uri(uri), info.Source) + "";
        }

        return info!;
    }
    #endregion

    #region 下行通知
    /// <summary>下行通知</summary>
    /// <returns></returns>
    [HttpGet(nameof(Notify))]
    public virtual async Task Notify()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();

            await HandleNotify(socket, HttpContext.RequestAborted);
        }
        else
        {
            HttpContext.Response.StatusCode = 400;
        }
    }

    /// <summary>长连接处理</summary>
    /// <param name="socket"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    protected virtual async Task HandleNotify(WebSocket socket, CancellationToken cancellationToken)
    {
        var device = Context.Device ?? throw new InvalidOperationException("未登录!");
        var sessionManager = _sessionManager ?? throw new InvalidOperationException("未找到SessionManager服务");

        using var session = new WsCommandSession(socket)
        {
            Code = device.Code,
            Log = this,
            SetOnline = online => _deviceService.SetOnline(Context, online)
        };

        sessionManager.Add(session);

        await session.WaitAsync(HttpContext, cancellationToken);
    }

    /// <summary>设备端响应服务调用</summary>
    /// <param name="model">服务</param>
    /// <returns></returns>
    [HttpPost(nameof(CommandReply))]
    public virtual Int32 CommandReply(CommandReplyModel model) => _deviceService.CommandReply(Context, model);

    /// <summary>向节点发送命令。通知节点更新、安装和启停应用等</summary>
    /// <param name="model">命令模型</param>
    /// <returns></returns>
    [AllowAnonymous]
    [HttpPost(nameof(SendCommand))]
    public Task<CommandReplyModel?> SendCommand(CommandInModel model)
    {
        if (model.Code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Code), "必须指定编码");
        if (model.Command.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Command));

        return _deviceService.SendCommand(Context, model, HttpContext.RequestAborted);
    }
    #endregion

    #region 事件上报
    /// <summary>批量上报事件</summary>
    /// <param name="events">事件集合</param>
    /// <returns></returns>
    [HttpPost(nameof(PostEvents))]
    public virtual Int32 PostEvents(EventModel[] events) => _deviceService.PostEvents(Context, events);
    #endregion

    #region 辅助
    /// <summary>写日志</summary>
    /// <param name="action"></param>
    /// <param name="success"></param>
    /// <param name="message"></param>
    public override void WriteLog(String action, Boolean success, String message) => _deviceService.WriteHistory(Context, action, success, message);
    #endregion
}