using AgileConfig.Client;
using Dpz.Core.AspNetCore;
using Dpz.Core.Infrastructure;
using Dpz.Core.Infrastructure.Configuration;
using Dpz.Core.Service;
using Dpz.Core.Web.Jobs;
using Dpz.Core.Web.Jobs.Hangfire;
using Dpz.Core.Web.Jobs.Hubs;
using Dpz.Core.Web.Jobs.Security;
using Microsoft.AspNetCore.HttpOverrides;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Serilog;
using Serilog.Events;

Log.Logger = new LoggerConfiguration().Enrich.FromLogContext().CreateBootstrapLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseAgileConfig(new ConfigClient(builder.Configuration));

    // 开发环境下重新加载本地配置,覆盖配置中心的特定节点
    if (builder.Environment.IsDevelopment())
    {
        builder.Configuration.AddJsonFile(
            "appsettings.Development.json",
            optional: true,
            reloadOnChange: true
        );
    }

    // Add services to the container.
    var services = builder.Services;
    var configuration = builder.Configuration;
    var objectSerializer = new ObjectSerializer(_ => true);
    BsonSerializer.RegisterSerializer(objectSerializer);

    #region configuration logs

    bool LogFilter(LogEvent logEvent)
    {
        // 检查请求路径
        if (
            logEvent.Properties.TryGetValue("RequestPath", out var requestPath)
            && requestPath is ScalarValue pathValue
        )
        {
            var pathPrefixes = new[] { "/jobs", "/account", "/health" };
            var path = pathValue.Value?.ToString();
            if (
                path != null
                && pathPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase))
            )
            {
                return true;
            }
        }

        // 检查源上下文
        if (
            logEvent.Properties.TryGetValue("SourceContext", out var source)
            && source is ScalarValue sourceValue
        )
        {
            var sourceStr = sourceValue.Value?.ToString();
            if (sourceStr != null)
            {
                var validPrefixes = new[]
                {
                    "Dpz.Core.Web.Jobs.Services.Implements",
                    "Dpz.Core.Web.Jobs.Services.Implements.JobService",
                    "Hangfire.Mongo.MongoNotificationObserver",
                    "Hangfire.Mongo.DistributedLock.MongoDistributedLock",
                };

                return validPrefixes.Any(p =>
                    sourceStr.StartsWith(p, StringComparison.OrdinalIgnoreCase)
                );
            }
        }

        return false;
    }
    var logSeq = configuration.GetSection("LogSeq").Get<LogSeq>();
    builder.Host.ConfigurationLog(logSeq, LogFilter);

    #endregion

    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        options.KnownIPNetworks.Clear();
        options.KnownProxies.Clear();
    });

    services.AddBusinessServices(configuration);
    services.AddDefaultServices().AddDefaultInjectServices().AddMessageQueueServices();

    services.AddOpenIddictClient(configuration);
    services.AddAppHangfireService(configuration);
    services.AddControllersWithViews();
    services.AddSignalR(options =>
    {
        // 增大 SignalR 消息体限制到 1MB,支持 Base64 文件分片传输
        options.MaximumReceiveMessageSize = 1024 * 1024;
    });

    var app = builder.Build();

    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseForwardedHeaders();

    app.UseSerilogRequestLogging();

    // 容器通过本机 HTTP 探测存活状态,其余请求保持 HTTPS 重定向
    app.UseWhen(
        context => context.Request.Path != "/health",
        branch => branch.UseHttpsRedirection()
    );
    app.UseStaticFiles();

    app.UseCors(appBuilder =>
    {
        appBuilder
            .WithOrigins(configuration.GetSection("Origins").Get<string[]>() ?? [])
            .AllowAnyHeader()
            .WithMethods("GET", "PUT", "POST", "DELETE", "PATCH", "OPTION")
            .AllowCredentials();
    });

    app.UseRouting();

    //启用身份认证
    app.UseAuthentication();
    //启用身份授权
    app.UseAuthorization();

    app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
    // 文件传输 SignalR Hub 终结点
    app.MapHub<FileTransferHub>("/transfer/hub");
    // CI/CD pipeline
    app.MapHub<PipelineHub>("/pipeline-notification");
    app.UseDevToolsEndpoint();
    app.UseVersionEndpoint();
    app.MapGet("/health", () => Results.Text("Healthy", "text/plain")).AllowAnonymous();

    app.HangfireSetting();

    app.Run();
}
catch (Exception ex)
{
    Console.Error.WriteLine(ex);
    Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

public partial class Program
{
    /// <summary>
    /// 控制台地址
    /// </summary>
    public static string ConsoleUrl => "/jobs";
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

报告:src/Dpz.Core.Web.Jobs/Program.cs

一、文件定位

这是 Dpz.Core.Web.Jobs 这个「后台任务/定时作业宿主」的进程入口(top-level statements + 末尾 public partial class Program)。它对应项目 Dpz.Core.Web.Jobs.csproj(Microsoft.NET.Sdk.Web,net11.0),承担三类职责:

  1. 用统一约定装配整个应用的依赖注入、配置源与日志(AgileConfig + Serilog + Seq);
  2. 搭建 ASP.NET Core 请求管线(反向代理头、HTTPS、CORS、认证授权、SignalR);
  3. 把 Hangfire Dashboard(路径 /jobs)和一批周期任务挂载到应用上。

它自身不实现业务逻辑,而是"编排者":真正的实现分散在同目录/相邻项目里。


二、逐段职责

1) 日志引导(文件首尾)

Log.Logger = new LoggerConfiguration().Enrich.FromLogContext().CreateBootstrapLogger();
...
catch (Exception ex) { Console.Error.WriteLine(ex); Log.Fatal(ex, "Host terminated unexpectedly"); }
finally { Log.CloseAndFlush(); }

在任何 DI 容器建立之前先设置一个"引导 logger",保证构建/启动阶段的异常也能被记录;无论成功失败,finally 都会冲刷并关闭 Serilog。真正的 Sink 与级别由后面的 ConfigurationLog 接管。

2) 配置源

  • builder.Host.UseAgileConfig(new ConfigClient(builder.Configuration)):接入 AgileConfig 配置中心(对应 AgileConfig.Client 包引用)。
  • 仅在 Development 下再 AddJsonFile("appsettings.Development.json", reloadOnChange: true),用于本地覆盖配置中心的部分节点(注释已说明)。

3) MongoDB BSON 序列化器

var objectSerializer = new ObjectSerializer(_ => true);
BsonSerializer.RegisterSerializer(objectSerializer);

为 object 类型注册"接受一切"的序列化器,供 Mongo 持久化那些弱类型字段使用(具体消费方在数据访问层,本文件不涉及)。

4) 日志过滤 + Serilog 落地

  • 局部函数 LogFilter(LogEvent):当事件带有 RequestPath(前缀为 /jobs、/account、/health)或 SourceContext(前缀为 Dpz.Core.Web.Jobs.Services.Implements、...JobService、Hangfire.Mongo.MongoNotificationObserver、Hangfire.Mongo.DistributedLock.MongoDistributedLock)时返回 true。
  • 从配置读取 LogSeq 节点并调用 builder.Host.ConfigurationLog(logSeq, LogFilter),实现见 LogConfiguration.cs。该扩展会把 Serilog 的 Sink 分成三条:文件(CompactJson,不做过滤)、控制台、Seq,同时按 Program/Version/Server 做属性富化,并把 Dpz.Core.Web.Jobs.Services.Implements 提到 Verbose。

需要注意一个语义细节(已通过 LogConfiguration 源码确认):传入的委托被用在 lc.Filter.ByExcluding(filter) 中,即 返回 true 的日志会被从控制台/Seq 排除(文件 sink 仍完整保留)。因此这里的 LogFilter 实际是"排除清单"(例如把 /health、/account 探针请求噪声挡在 Seq 之外),而非"仅保留这些"。

5) 反向代理与转发头

ForwardedHeadersOptions 只启用 XForwardedFor | XForwardedProto,并清空 KnownIPNetworks / KnownProxies,表示信任前置 nginx/网关传入的转发头(同一仓库其他宿主如 Dpz.Core.Auth/Program.cs 有类似写法)。

6) 服务注册

services.AddBusinessServices(configuration);                                  // Dpz.Core.Service
services.AddDefaultServices().AddDefaultInjectServices().AddMessageQueueServices(); // 仓库/源生成 + Web.Jobs 专有 + 消息队列
services.AddOpenIddictClient(configuration);   // Web.Jobs 的 OIDC 客户端
services.AddAppHangfireService(configuration);// Hangfire
services.AddControllersWithViews();
services.AddSignalR(o => o.MaximumReceiveMessageSize = 1024 * 1024);
  • AddBusinessServices 定义在 ServiceExtensions.cs;
  • AddDefaultServices 定义在 ServiceDependencyInjection.cs(仓库层/源生成注册);
  • AddDefaultInjectServices / AddMessageQueueServices 定义在本项目 RegisterDependencyInjectionExtensions.cs:前者注册文件传输房间/会话服务与清理 HostedService、备份记录服务、若干具名 HttpClient(edge、音乐同步、PipelineVersionProbe)、S3/备份/网络/MCP 等;后者注册一组 AddMessageConsumer<消息, 处理器>(处理器位于 MessageHandlers)以及 Outbox 补发 Worker;
  • AddOpenIddictClient 见 RegisterOpenIddictExtensions.cs:Cookie 认证 + OpenIddict 授权码流程客户端,并把 JobsToken 的 TokenStore 替换为服务层的实现,配置来自 Client:Jobs:* 与 Server:Issuer,缺失即抛 InvalidConfigurationException;
  • AddAppHangfireService 见 HangfireConfiguration.cs:以 hangfire 配置节初始化 Hangfire,并注册 HangfireAuthorizationFilter.cs 为单例。

7) HTTP 请求管线与端点

  • 非开发环境:UseExceptionHandler("/Home/Error") + UseHsts();
  • UseForwardedHeaders() → UseSerilogRequestLogging();
  • 条件化 HTTPS 重定向:UseWhen(ctx => ctx.Request.Path != "/health", branch => branch.UseHttpsRedirection()),让容器内的 HTTP 存活探测不被 307 重定向;
  • UseStaticFiles();CORS 从配置 Origins 读取白名单,允许任意 Header 与凭据,方法列表为 GET/PUT/POST/DELETE/PATCH/OPTION;
  • UseRouting() → UseAuthentication() → UseAuthorization();
  • 默认 MVC 路由 {controller=Home}/{action=Index}/{id?}(对应 Controllers 与 Views);
  • 两个 SignalR Hub:MapHub<FileTransferHub>("/transfer/hub")、MapHub<PipelineHub>("/pipeline-notification")(见 FileTransferHub.cs、PipelineHub.cs,后者带 [Authorize]);
  • UseDevToolsEndpoint()(Chrome DevTools workspace 描述文件,仅开发环境给真实路径)与 UseVersionEndpoint()(/api/version),实现见 AspNetCoreServiceCollectionExtensions.cs;
  • MapGet("/health", ...).AllowAnonymous() 返回纯文本 Healthy;
  • 最后 app.HangfireSetting():设置 zh-CN 本地化 → 先过 HangfireDashboardAccessMiddleware.cs 做登录/权限校验 → 在 Program.ConsoleUrl(即 /jobs)挂载 Hangfire Dashboard,并按配置节 hangfireStatus:* 开关注册/移除一整套 RecurringJob(IT之家抓取、Steam、备份、COS 空目录清理、代码结构同步、图片元信息回填、Outbox 清理、音乐同步等)。

8) 末尾的 public partial class Program

ConsoleUrl => "/jobs" 是 Dashboard 路径的唯一真源,被三处引用:HangfireConfiguration.cs(UseHangfireDashboard)、HangfireDashboardAccessMiddleware.cs(StartsWithSegments 前置于末尾)、以及视图中(如 Views/Home/Index.cshtml、Views/Pipeline/Index.cshtml 的"控制台"链接)。把 top-level Program 声明为 partial 也让它对外可见(常见用于集成测试或外部引用;本仓库当前未搜索到 WebApplicationFactory 用法,因此不臆断存在相关测试)。


三、启动流程

flowchart TD
    A[引导 Serilog BootstrapLogger] --> B[CreateBuilder]
    B --> C[UseAgileConfig 配置中心]
    C --> D{IsDevelopment?}
    D -- 是 --> E[追加 appsettings.Development.json 覆盖]
    D -- 否 --> F[跳过]
    E --> G[注册 Bson ObjectSerializer]
    F --> G
    G --> H[ConfigurationLog 配置 Serilog 文件/控制台/Seq + 过滤器]
    H --> I[Configure ForwardedHeaders 信任代理]
    I --> J[AddBusinessServices / AddDefaultServices / AddDefaultInjectServices / AddMessageQueueServices]
    J --> K[AddOpenIddictClient 认证]
    K --> L[AddAppHangfireService]
    L --> M[AddControllersWithViews + AddSignalR 1MB]
    M --> N[Build]
    N --> O{非开发环境?}
    O -- 是 --> P[UseExceptionHandler + UseHsts]
    O -- 否 --> Q[跳过]
    P --> R[UseForwardedHeaders + UseSerilogRequestLogging]
    Q --> R
    R --> S["UseWhen(path != /health) UseHttpsRedirection"]
    S --> T[StaticFiles + CORS + Routing]
    T --> U[Authentication + Authorization]
    U --> V[MVC 默认路由]
    V --> W["MapHub FileTransferHub /transfer/hub"]
    W --> X["MapHub PipelineHub /pipeline-notification"]
    X --> Y[DevTools 端点 + /api/version]
    Y --> Z["MapGet /health 匿名"]
    Z --> AA["HangfireSetting: 本地化 → DashboardAccess → /jobs 面板 + RecurringJob 注册"]
    AA --> AB[app.Run 阻塞运行]
    AB -.异常.-> AC[Log.Fatal + Console.Error]
    AC --> AD[finally: CloseAndFlush 冲刷日志]

四、与相关文件的协作关系

协作方关系
RegisterDependencyInjectionExtensions.cs被 AddDefaultInjectServices / AddMessageQueueServices 调用,注入 Web.Jobs 专有服务、HttpClient、消息消费者与 Outbox Worker
HangfireConfiguration.cs被 AddAppHangfireService / HangfireSetting 调用,Hangfire 存储、面板与周期任务的落地处
Security/RegisterOpenIddictExtensions.cs被 AddOpenIddictClient 调用,提供 Cookie + OpenIddict 客户端认证
HangfireDashboardAccessMiddleware.cs / HangfireAuthorizationFilter.cs依赖 Program.ConsoleUrl,在 Dashboard 之前拦截未登录/无权限访问
Hubs/FileTransferHub.cs、Hubs/PipelineHub.cs被 MapHub 暴露为 SignalR 端点
Dpz.Core.Infrastructure/Configuration/LogConfiguration.csConfigurationLog 的实现,消费本文件的 LogFilter 与 LogSeq 配置
Dpz.Core.AspNetCore/AspNetCoreServiceCollectionExtensions.cs提供 UseDevToolsEndpoint / UseVersionEndpoint
Dpz.Core.Service/ServiceExtensions.cs、ServiceDependencyInjection.cs提供 AddBusinessServices、AddDefaultServices
Views/Home/Index.cshtml、Views/Pipeline/Index.cshtml渲染页面时读取 Program.ConsoleUrl 生成"控制台"链接
docker-compose.yaml、Dockerfile、README.md部署侧配套;/health 与转发头处理与之呼应

五、观察到的注意点(不做臆断,仅陈述代码事实)

  1. CORS 方法列表写的是 "OPTION"(缺少结尾 S)。.WithMethods(...) 是按字面匹配的,因此标准 OPTIONS 预检请求可能不被该策略放行,需要结合部署中间件确认是否由代理层处理预检。
  2. LogFilter 被当作 ByExcluding 的排除条件使用(见上文"语义细节"),如果你的预期是"仅保留这些日志进入 Seq/控制台",则与当前行为相反。
  3. LogFilter 中对 SourceContext 的第二个前缀 "Dpz.Core.Web.Jobs.Services.Implements.JobService" 是第一个前缀 "Dpz.Core.Web.Jobs.Services.Implements" 的子集,属冗余项,不影响行为。
  4. HangfireSetting() 中的多组 RecurringJob.AddOrUpdate(...) 在 hangfireStatus:* 关闭时会执行 RemoveIfExists,即配置开关会反向删除已有周期任务,是运行期行为,不只是"启动时不注册"。
评论加载中...