调整定时作业分布式锁Key名称规则
猿人易 编写于 2024-05-06 13:14:10
NewLife.Cube
using System.Collections.Concurrent;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.WebEncoders;
using Microsoft.Net.Http.Headers;

using NewLife.Caching;
using NewLife.Common;
using NewLife.Cube.Extensions;
using NewLife.Cube.Jobs;
using NewLife.Cube.Modules;
using NewLife.Cube.Services;
using NewLife.Cube.WebMiddleware;
using NewLife.IP;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Serialization;
using NewLife.Web;

using Stardust;
using Stardust.Registry;

using XCode;
using XCode.DataAccessLayer;

namespace NewLife.Cube;

/// <summary>魔方服务</summary>
public static class CubeService
{
    /// <summary>区域名集合</summary>
    public static String[] AreaNames { get; set; }

    #region 配置魔方
    /// <summary>添加魔方,放在AddControllersWithViews之后</summary>
    /// <param name="services"></param>
    /// <returns></returns>o
    public static IServiceCollection AddCube(this IServiceCollection services)
    {
        // 引入星尘
        if (!services.Any(e => e.ServiceType == typeof(StarFactory)))
        {
            var star = new StarFactory();
            services.AddSingleton(star);
            services.AddSingleton(P => star.Tracer);
            services.AddSingleton(P => star.Config);
            services.AddSingleton(p => star.Service);
        }

        // 检查是否延迟启动,可能是重启或更新
        var args = Environment.GetCommandLineArgs();
        if ("-delay".EqualIgnoreCase(args)) Thread.Sleep(3000);

        using var span = DefaultTracer.Instance?.NewSpan(nameof(AddCube));

        XTrace.WriteLine("{0} Start 配置魔方 {0}", new String('=', 32));
        Assembly.GetExecutingAssembly().WriteVersion();

        // 修正系统名,确保可运行
        var sys = SysConfig.Current;
        if (sys.Name == "NewLife.Cube.Views" || sys.DisplayName == "NewLife.Cube.Views")
        {
            sys.Name = "NewLife.Cube";
            sys.DisplayName = "魔方平台";
            sys.Save();
        }

        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        //IpResolver.Register();

        // 连接字符串
        DAL.ConnStrs.TryAdd("Cube", "MapTo=Membership");

        // 配置Cookie策略
        services.ConfigureNonBreakingSameSiteCookies();
        //services.Configure<CookiePolicyOptions>(options =>
        //{
        //    // 此项为true需要用户授权才能记录cookie
        //    options.CheckConsentNeeded = context => false;
        //    options.MinimumSameSitePolicy = SameSiteMode.None;
        //});

        // 添加Session会话支持
        services.AddSession();

        // 身份验证
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "Cube";
            options.DefaultAuthenticateScheme = "Cube";
            options.DefaultChallengeScheme = "Cube";
            options.DefaultSignInScheme = "Cube";
        }).AddCookie("Cube", options =>
        {
            options.AccessDeniedPath = "/Admin/User/Login";
            options.LoginPath = "/Admin/User/Login";
            options.LogoutPath = "/Admin/User/Logout";
        });

        //// 注册魔方默认UI
        //services.AddCubeDefaultUI();

        var set = CubeSetting.Current;
        services.AddSingleton(set);

        // 配置跨域处理,允许所有来源
        // CORS,全称 Cross-Origin Resource Sharing (跨域资源共享),是一种允许当前域的资源能被其他域访问的机制
        if (set.CorsOrigins == "*")
            services.AddCors(options => options.AddPolicy("cube_cors", builder => builder
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials()
            .SetIsOriginAllowed(hostname => true)));
        else if (!set.CorsOrigins.IsNullOrEmpty())
            services.AddCors(options => options.AddPolicy("cube_cors", builder => builder
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials()
            .WithOrigins(set.CorsOrigins)));

        services.Configure<MvcOptions>(opt =>
        {
            opt.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());

            // 分页器绑定
            opt.ModelBinderProviders.Insert(0, new PagerModelBinderProvider());

            // 模型绑定
            opt.ModelBinderProviders.Insert(0, new EntityModelBinderProvider());
        });

        services.AddCustomApplicationParts();

        // 添加管理提供者
        services.AddManageProvider();

        // 添加数据保护,优先在外部支持Redis持久化,这里默认使用数据库持久化
        //if (services.Any(e => e.ServiceType == typeof(FullRedis) || e.ServiceType == typeof(ICacheProvider) && e.ImplementationType == typeof(RedisCacheProvider)))
        //    services.AddDataProtection().PersistKeysToRedis();
        //else
        //    services.AddDataProtection().PersistKeysToDb();
        services.AddDataProtection()
            .PersistKeysToDb();

        // 防止汉字被自动编码
        services.Configure<WebEncoderOptions>(options =>
        {
            options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All);
        });

        // 配置视图引擎
        services.Configure<RazorViewEngineOptions>(o =>
        {
            o.ViewLocationExpanders.Add(new ThemeViewLocationExpander());
        });

        // 配置Json
        services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
        {
#if NET7_0_OR_GREATER
            // 支持模型类中的DataMember特性
            options.JsonSerializerOptions.TypeInfoResolver = DataMemberResolver.Default;
#endif
            options.JsonSerializerOptions.Converters.Add(new TypeConverter());
            options.JsonSerializerOptions.Converters.Add(new LocalTimeConverter());
            // 支持中文编码
            options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
        });
        //默认注入缓存实现
        services.TryAddSingleton<ICacheProvider, CacheProvider>();

        // UI服务
        services.AddSingleton<UIService>();
        services.AddSingleton<PasswordService>();
        services.AddSingleton<UserService>();

        //services.AddHostedService<JobService>();
        services.AddHostedService<DataRetentionService>();

        // 添加定时作业
        services.AddCubeJob();

        // 注册IP地址库
        IpResolver.Register();

        // 插件
        var moduleManager = new ModuleManager();
        services.AddSingleton(moduleManager);
        var modules = moduleManager.LoadAll(services);
        if (modules.Count > 0)
        {
            XTrace.WriteLine("加载功能插件[{0}]个", modules.Count);
            foreach (var item in modules)
            {
                XTrace.WriteLine("加载插件:{0}", item.Key);
                item.Value.Add(services);
            }
        }

        XTrace.WriteLine("{0} End   配置魔方 {0}", new String('=', 32));

        return services;
    }

    /// <summary>添加自定义应用部分,即添加外部引用的控制器、视图的Assembly,作为本应用的一部分</summary>
    /// <param name="services"></param>
    public static void AddCustomApplicationParts(this IServiceCollection services)
    {
        using var span = DefaultTracer.Instance?.NewSpan(nameof(AddCustomApplicationParts));

        var manager = services.LastOrDefault(e => e.ServiceType == typeof(ApplicationPartManager))?.ImplementationInstance as ApplicationPartManager;
        manager ??= new ApplicationPartManager();

        var list = FindAllArea();

        foreach (var asm in list)
        {
            XTrace.WriteLine("注册区域视图程序集:{0}", asm.FullName);

            var factory = ApplicationPartFactory.GetApplicationPartFactory(asm);
            foreach (var part in factory.GetApplicationParts(asm))
            {
                if (!manager.ApplicationParts.Contains(part)) manager.ApplicationParts.Add(part);
            }
        }
    }

    /// <summary>遍历所有引用了AreaRegistrationBase的程序集</summary>
    /// <returns></returns>
    private static List<Assembly> FindAllArea()
    {
        var bag = new ConcurrentBag<Assembly>();
        var baseType = typeof(ControllerBaseX);
        var baseType2 = typeof(RazorPage);
        Parallel.ForEach(AppDomain.CurrentDomain.GetAssemblies(), asm =>
        {
            try
            {
                foreach (var type in asm.GetTypes())
                {
                    if (type.IsInterface || type.IsAbstract || type.IsGenericType) continue;
                    if (type != baseType && type.As(baseType) || type.As(baseType2))
                    {
                        bag.Add(asm);
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                XTrace.WriteLine("在[{0}]中扫描视图报错:{1}", asm.GetName().Name, ex.Message);
            }
        });
        var list = bag.ToList();

#if !NET6_0_OR_GREATER
        // 反射 *.Views.dll
        foreach (var item in ".".AsDirectory().GetFiles("*.Views.dll"))
        {
            var asm = Assembly.LoadFile(item.FullName);
            if (!list.Contains(asm) && !list.Any(e => e.FullName == asm.FullName))
            {
                list.Add(asm);
            }
        }

        // 反射 NewLife.Cube.*.dll
        foreach (var item in ".".AsDirectory().GetFiles("NewLife.Cube.*.dll"))
        {
            var asm = Assembly.LoadFile(item.FullName);
            if (!list.Contains(asm) && !list.Any(e => e.FullName == asm.FullName))
            {
                list.Add(asm);
            }
        }
#endif

        // 为了能够实现模板覆盖,程序集相互引用需要排序,父程序集在前
        list.Sort((x, y) =>
        {
            if (x == y) return 0;
            if (x != null && y == null) return 1;
            if (x == null && y != null) return -1;

            //return x.GetReferencedAssemblies().Any(e => e.FullName == y.FullName) ? 1 : -1;
            // 对程序集引用进行排序时,不能使用全名,当魔方更新而APP没有重新编译时,版本的不同将会导致全名不同,无法准确进行排序
            var yname = y.GetName().Name;
            return x.GetReferencedAssemblies().Any(e => e.Name == yname) ? 1 : -1;
        });

        return list;
    }
    #endregion

    #region 使用魔方
    /// <summary>使用魔方,放在UseEndpoints之前,自动探测是否UseRouting</summary>
    /// <param name="app"></param>
    /// <param name="env"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseCube(this IApplicationBuilder app, IWebHostEnvironment env)
    {
        var provider = app.ApplicationServices;
        using var span = DefaultTracer.Instance?.NewSpan(nameof(UseCube));

        XTrace.WriteLine("{0} Start 初始化魔方 {0}", new String('=', 32));

        // 初始化数据库连接
        var set = CubeSetting.Current;
        if (set.IsNew)
            EntityFactory.InitAll();
        else
            EntityFactory.InitAllAsync();

        // 调整魔方表名
        FixAppTableName();
        FixAvatar();
        WebHelper2.FixTenantMenu();

        // 使用管理提供者
        app.UseManagerProvider();

        //FixOAuth();

        // 使用Cube前添加自己的管道
        if (env != null)
        {
            // 使用自己的异常处理页,后续必须再次UseRouting
            if (!env.IsDevelopment())
                app.UseExceptionHandler("/CubeHome/Error");
        }

        // 设置X-Frame-Options
        app.Use(async (context, next) =>
        {
            if (!set.XFrameOptions.IsNullOrWhiteSpace())
            {
                context.Response.Headers[HeaderNames.XFrameOptions] = set.XFrameOptions;
            }

            await next();
        });

        if (!set.CorsOrigins.IsNullOrEmpty()) app.UseCors("cube_cors");

        // 配置静态Http上下文访问器
        app.UseStaticHttpContext();

        // 注册中间件
        //app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseSession();
        app.UseAuthentication();

        // 如果已引入追踪中间件,则这里不再引入
        TracerMiddleware.Tracer ??= DefaultTracer.Instance;
        if (TracerMiddleware.Tracer != null && !app.Properties.ContainsKey(nameof(TracerMiddleware)))
        {
            app.UseMiddleware<TracerMiddleware>();

            app.Properties[nameof(TracerMiddleware)] = typeof(TracerMiddleware);
        }

        app.UseMiddleware<RunTimeMiddleware>();
        app.UseMiddleware<TenantMiddleware>();

        if (env != null) app.UseCubeDefaultUI(env);

        // 设置默认路由。如果外部已经执行 UseRouting,则直接注册
        app.UseRouter(endpoints =>
        {
            XTrace.WriteLine("注册魔方区域路由");

            endpoints.MapControllerRoute(
                "CubeAreas",
                "{area}/{controller=Index}/{action=Index}/{id?}");
        });

        ManageProvider2.EndpointRoute = (IEndpointRouteBuilder)app.Properties["__EndpointRouteBuilder"];

        // 自动检查并添加菜单
        AreaBase.RegisterArea<Admin.AdminArea>();
        AreaBase.RegisterArea<Cube.CubeArea>();

        // 插件
        var moduleManager = provider.GetRequiredService<ModuleManager>();
        var modules = moduleManager.LoadAll();
        if (modules.Count > 0)
        {
            XTrace.WriteLine("启用功能插件[{0}]个", modules.Count);
            foreach (var item in modules)
            {
                XTrace.WriteLine("启用插件:{0}", item.Key);
                item.Value.Use(app, env);
            }
        }

        XTrace.WriteLine("{0} End   初始化魔方 {0}", new String('=', 32));

        Task.Run(() => ResolveStarWeb(provider));

        // 注册退出事件
        if (app is IHost web)
            NewLife.Model.Host.RegisterExit(() =>
            {
                XTrace.WriteLine("魔方优雅退出!");
                web.StopAsync().Wait();
            });

        return app;
    }

    private static void FixAppTableName()
    {
        try
        {
            var dal = DAL.Create("Cube");
            var tables = dal.Tables;
            if (tables != null && !tables.Any(e => e.TableName.EqualIgnoreCase("OAuthApp")))
            {
                XTrace.WriteLine("未发现OAuth应用新表 OAuthApp");

                // 验证表名和部分字段名,避免误改其它表
                var dt = tables.FirstOrDefault(e => e.TableName.EqualIgnoreCase("App"));
                if (dt != null && dt.Columns.Any(e => e.ColumnName.EqualIgnoreCase("RoleIds")))
                {
                    XTrace.WriteLine("发现OAuth应用旧表 App ,准备重命名");

                    var rs = dal.Execute($"Alter Table App Rename To OAuthApp");
                    XTrace.WriteLine("重命名结果:{0}", rs);
                }
            }
        }
        catch (Exception ex)
        {
            XTrace.WriteException(ex);
        }
    }

    /// <summary>修正头像附件,移动到附件上传目录</summary>
    private static void FixAvatar()
    {
        var set = CubeSetting.Current;
        if (set.AvatarPath.IsNullOrEmpty()) return;

        var av = set.AvatarPath.CombinePath("User").AsDirectory();
        if (!av.Exists) return;

        // 如果附件目录跟头像目录一致,则不需要移动
        var dst = set.UploadPath.CombinePath("User").AsDirectory();
        if (dst.FullName.EqualIgnoreCase(av.FullName)) return;

        try
        {
            // 确保目标目录存在
            dst.Parent.FullName.EnsureDirectory(false);

            //av.MoveTo(dst.FullName);
            //av.CopyTo(dst.FullName, null, true, e => XTrace.WriteLine("移动 {0}", e));

            // 来源目录根,用于截断
            var root = av.FullName.EnsureEnd(Path.DirectorySeparatorChar.ToString());
            foreach (var item in av.GetAllFiles(null, true))
            {
                var name = item.FullName.TrimStart(root);
                var dfile = dst.FullName.CombinePath(name);
                if (!File.Exists(dfile))
                {
                    XTrace.WriteLine("移动 {0}", name);
                    item.MoveTo(dfile.EnsureDirectory(true), false);
                }
                else
                {
                    item.Delete();
                }

                //if (item.Exists) item.Delete();
                //if (File.Exists(item.FullName)) File.Delete(item.FullName);
            }

            // 删除空目录
            if (!av.GetAllFiles(null, true).Any()) av.Delete(true);
        }
        catch (Exception ex)
        {
            XTrace.WriteException(ex);
        }
    }

    //private static void FixOAuth()
    //{
    //    var list = OAuthConfig.FindAllWithCache();
    //    foreach (var cfg in list)
    //    {
    //        if (cfg.Server.StartsWithIgnoreCase("https://sso.newlifex.com/sso"))
    //        {
    //            cfg.Server = "https://sso.newlifex.com/sso,http://sso2.newlifex.com/sso";
    //            cfg.Update();
    //        }
    //    }
    //}

    /// <summary>使用魔方首页</summary>
    /// <param name="app"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseCubeHome(this IApplicationBuilder app)
    {
        app.UseRouter(endpoints =>
        {
            endpoints.MapControllerRoute(
                "Default",
                "{controller=CubeHome}/{action=Index}/{id?}"
                );
        });

        return app;
    }

    /// <summary>使用路由配置,用于注册路由映射</summary>
    /// <param name="app"></param>
    /// <param name="configure"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseRouter(this IApplicationBuilder app, Action<IEndpointRouteBuilder> configure)
    {
        if (configure == null) throw new ArgumentNullException(nameof(configure));

        // 设置默认路由。如果外部已经执行 UseRouting,则直接注册
        if (app.Properties.TryGetValue("__EndpointRouteBuilder", out var value) && value is IEndpointRouteBuilder eps)
        {
            configure(eps);
        }
        else
        {
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                configure(endpoints);
            });
        }

        return app;
    }

    static async Task ResolveStarWeb(IServiceProvider provider)
    {
        // 消费StarWeb服务地址,如果未接入星尘,这里也没必要获取这个地址
        var registry = provider.GetService<IRegistry>();
        if (registry != null)
        {
            using var span = DefaultTracer.Instance?.NewSpan(nameof(ResolveStarWeb));
            try
            {
                var webs = await registry.ResolveAddressAsync("StarWeb");
                if (webs != null)
                {
                    XTrace.WriteLine("StarWeb: {0}", webs.Join());
                    //StarHelper.StarWeb = webs.FirstOrDefault();
                    if (webs.Length > 0)
                    {
                        // 保存到配置文件
                        var set = CubeSetting.Current;
                        set.StarWeb = webs[0];
                        set.Save();
                    }
                }
            }
            catch (Exception ex)
            {
                span?.SetError(ex, null);
                XTrace.WriteLine(ex.Message);
            }
        }
    }
    #endregion
}