using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection.Extensions; using NPOI.POIFS.Crypt; using OASystem.API.Middlewares; using OASystem.API.OAMethodLib; using OASystem.API.OAMethodLib.AMapApi; using OASystem.API.OAMethodLib.APNs; using OASystem.API.OAMethodLib.DeepSeekAPI; using OASystem.API.OAMethodLib.GenericSearch; using OASystem.API.OAMethodLib.Hotmail; using OASystem.API.OAMethodLib.Hub.Hubs; using OASystem.API.OAMethodLib.HunYuanAPI; using OASystem.API.OAMethodLib.JuHeAPI; using OASystem.API.OAMethodLib.QiYeWeChatAPI; using OASystem.API.OAMethodLib.Quartz.Jobs; using OASystem.API.OAMethodLib.SignalR.HubService; using OASystem.Infrastructure.Logging; using Quartz; using Quartz.Impl; using Quartz.Spi; using QuzrtzJob.Factory; using Serilog.Events; using System.IO.Compression; using TencentCloud.Common; using TencentCloud.Common.Profile; using TencentCloud.Hunyuan.V20230901; using static OASystem.API.Middlewares.RateLimitMiddleware; using OASystem.API.OAMethodLib.MicrosoftGraphMailbox; Console.Title = $"FMGJ OASystem Server"; var builder = WebApplication.CreateBuilder(args); var basePath = AppContext.BaseDirectory; // 导入配置文件 var _config = new ConfigurationBuilder() .SetBasePath(basePath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.AddSingleton(new AppSettingsHelper(_config)); // 设置请求参数发生异常 builder.Services.AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true); // 设置请求参数错误 默认返回格式 builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = context => { var errors = context.ModelState .Where(e => e.Value.Errors.Count > 0) .ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray() ); var result = new JsonView { Code = 400, Msg = errors.FirstOrDefault().Value.FirstOrDefault() ?? "", Data = errors }; return new BadRequestObjectResult(result); }; }); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddControllers() .AddJsonOptions(options => { // 空字段不响应 Response //options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.JsonSerializerOptions.Converters.Add(new NullJsonConverter()); // 时间格式化响应 options.JsonSerializerOptions.Converters.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss")); // decimal 四位小数 // 保留小数位数参数传递给自定义序列化器 //options.JsonSerializerOptions.Converters.Add(new DecimalConverter(_decimalPlaces)); }); builder.Services.TryAddSingleton(); #region 添加限流中间件服务注册 // 添加内存缓存,限流中间件使用 builder.Services.AddMemoryCache(); // 配置限流设置 builder.Services.Configure( builder.Configuration.GetSection("RateLimiting")); #endregion #region Gzip builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add(); }); builder.Services.Configure(options => { options.Level = CompressionLevel.Optimal; }); #endregion #region Cors builder.Services.AddCors(options => { //policy.AddPolicy("Cors", opt => opt // //.SetIsOriginAllowed(origin => // //{ // // // 定义允许的来源列表 // // var allowedOrigins = new List // // { // // "http://132.232.92.186:9002", // // "http://oa.pan-american-intl.com:4399" // // }; // // // 检查请求的来源是否在允许的列表中 // // return allowedOrigins.Contains(origin); // //}) // //.AllowAnyOrigin() // //.AllowAnyHeader() // //.WithMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS") // //.AllowCredentials()); // .AllowAnyHeader() // .AllowAnyMethod() // .AllowCredentials()); options.AddPolicy("Cors", policy => { policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); #endregion #region 上传文件 builder.Services.AddCors(policy => { policy.AddPolicy("Cors", opt => opt .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod() .WithExposedHeaders("X-Pagination")); }); builder.Services.Configure(options => { options.KeyLengthLimit = int.MaxValue; options.ValueLengthLimit = int.MaxValue; options.MultipartBodyLengthLimit = int.MaxValue; options.MultipartHeadersLengthLimit = int.MaxValue; }); builder.Services.Configure(options => { options.Limits.MaxRequestBodySize = int.MaxValue; options.Limits.MaxRequestBufferSize = int.MaxValue; }); #endregion #region 上传文件 // 上传文件分组配置:Tuple<分组标识, 分组名称> var groups = new List> { // 示例分组(取消注释即可启用) //new Tuple("Group1","分组一"), //new Tuple("Group2","分组二") }; #endregion #region 接口分组 #region old builder.Services.AddScoped(options => { return new SqlSugarClient(new List() { new() { ConfigId = DBEnum.OA2023DB, ConnectionString = _config.GetConnectionString("OA2023DB"), DbType = DbType.SqlServer, IsAutoCloseConnection = true, }, new() { ConfigId = DBEnum.OA2014DB, ConnectionString = _config.GetConnectionString("OA2014DB"), DbType = DbType.SqlServer, IsAutoCloseConnection = true }, } , db => { // SQL 执行完 db.Aop.OnLogExecuted = (sql, pars) => { // 超过 1 秒 if (db.Ado.SqlExecutionTime.TotalSeconds > 1) { var FirstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 执行完成可以输出 SQL 执行时间 (OnLogExecutedDelegate) Console.WriteLine("NowTime:" + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")); Console.WriteLine("MethodName:" + FirstMethodName); Console.WriteLine("ElapsedTime:" + db.Ado.SqlExecutionTime.ToString()); Console.WriteLine("ExecuteSQL:" + sql); } }; // db.Aop.OnLogExecuting = (sql, pars) => { }; // SQL 执行前 db.Aop.OnError = (exp) => { // 获取原生 SQL 建议 5.1.4.63 性能 OK //UtilMethods.GetNativeSql(exp.Sql, exp.Parametres); // 获取无参数 SQL 对性能有影响,特别是大的 SQL 参数多的,调试使用 //UtilMethods.GetSqlString(DbType.SqlServer, exp.sql, exp.parameters); }; // 修改 SQL 和参数的值 db.Aop.OnExecutingChangeSql = (sql, pars) => { return new KeyValuePair(sql, pars); }; } ); }); #endregion #endregion #region 注入 Swagger 注解 (禁用) if (AppSettingsHelper.Get("UseSwagger").ToBool()) { builder.Services.AddSwaggerGen(a => { a.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "Api", Description = "Api 接口文档" }); foreach (var item in groups) { a.SwaggerDoc(item.Item1, new OpenApiInfo { Version = item.Item1, Title = item.Item2, Description = $"{item.Item2}鎺ュ彛鏂囨。" }); } a.DocumentFilter(); a.IncludeXmlComments(Path.Combine(basePath, "OASystem.Api.xml"), true); a.IncludeXmlComments(Path.Combine(basePath, "OASystem.Domain.xml"), true); a.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "Value: Bearer {token}", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); a.AddSecurityRequirement(new OpenApiSecurityRequirement() {{ new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header }, new List() } }); }); } #endregion #region 添加校验 builder.Services.AddTransient(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = "OASystem.com", ValidIssuer = "OASystem.com", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtSecurityKey"])), ClockSkew = TimeSpan.FromSeconds(30), // 过期时间默认值,解决服务器时间不同步问题(秒) RequireExpirationTime = true, }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { var path = context.HttpContext.Request.Path; // 如果是 signalr 请求,需要将 token 迁移,否则 JWT 获取不到 token。OPTIONS 请求需要过滤到,因为 OPTIONS 请求获取不到 Token,用 NGINX 过滤掉 OPTIONS 请求。 if (path.StartsWithSegments("/ChatHub")) { string accessToken = context.Request.Query["access_token"].ToString(); if (string.IsNullOrWhiteSpace(accessToken)) { accessToken = context.Request.Headers["Authorization"].ToString(); } context.Token = accessToken.Replace("Bearer ", "").Trim(); } return Task.CompletedTask; } }; }); #endregion #region 初始化日志 var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); Log.Logger = new LoggerConfiguration() // 不记录定时访问API .Filter.ByIncludingOnly(logEvent => { if (logEvent.Properties.TryGetValue("RequestPath", out var pathValue)) { var path = pathValue.ToString().Trim('"'); return !path.StartsWith("/api/System/PotsMessageUnreadTotalCount"); } return true; }) .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console() .WriteTo.File(Path.Combine("Logs", @"Log.txt"), rollingInterval: RollingInterval.Day) .CreateLogger(); #region 出入境费用明细 专用记录器 // 出入境费用明细 专用记录器 var logDirectory = @"D:\OASystem\Logs\EnterExitCost"; // 自动创建目录(如果不存在) try { Directory.CreateDirectory(logDirectory); Log.Information($"日志目录已创建/确认存在: {logDirectory}"); } catch (Exception ex) { Log.Fatal($"无法创建日志目录{logDirectory}: {ex.Message}"); throw; } var eec_TextLogger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.File(Path.Combine(logDirectory, "text-records-.txt"), rollingInterval: RollingInterval.Month) .CreateLogger(); #endregion #region 分组步骤操作 专用记录器 // 指定磁盘绝对路径(示例:D盘的AppLogs文件夹) var groupLogDir = @"D:\OASystem\Logs\GroupStepOP"; // 自动创建目录(如果不存在) try { // 创建目录,若已存在则不执行任何操作 Directory.CreateDirectory(groupLogDir); // 记录日志:目录已创建/确认存在 Log.Information($"日志目录已创建/确认存在: {groupLogDir}"); } catch (Exception ex) { // 记录致命错误:无法创建日志目录 Log.Fatal($"无法创建日志目录 {groupLogDir}: {ex.Message}"); // 抛出异常终止程序 throw; } // 初始化分组步骤操作专用日志器 var groupStepOP_TextLogger = new LoggerConfiguration() .MinimumLevel.Information() // 最低日志级别:Information .WriteTo.File( // 输出到文件 Path.Combine(groupLogDir, "text-records-.txt"), // 日志文件路径+名称 rollingInterval: RollingInterval.Month) // 滚动规则:按月生成新文件 .CreateLogger(); // 创建日志实例 #endregion #region 任务分配操作 专用记录器 // 指定磁盘绝对路径(示例:D盘的AppLogs文件夹) var taskLogDir = @"D:\OASystem\Logs\TaskAllocation"; // 自动创建目录(如果不存在) try { Directory.CreateDirectory(taskLogDir); Log.Information($"日志目录已创建/确认存在: {taskLogDir}"); } catch (Exception ex) { Log.Fatal($"无法创建日志目录 {taskLogDir}: {ex.Message}"); throw; } // 创建任务分配专用日志实例(按月滚动归档) var task_TextLogger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.File( Path.Combine(taskLogDir, "text-records-.txt"), rollingInterval: RollingInterval.Month ) .CreateLogger(); #endregion // 閰嶇疆Serilog涓篖og; builder.Host.UseSerilog(); builder.Services.AddSingleton(new TextFileLogger(eec_TextLogger)); builder.Services.AddSingleton(new GroupTextFileLogger(groupStepOP_TextLogger)); builder.Services.AddSingleton(new TaskTextFileLogger(task_TextLogger)); #endregion #region 注入注册 Autofac 模块 // 使用 Autofac 作为 DI 容器工厂,替换默认容器 builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); // 配置 Autofac 容器注册 var hostBuilder = builder.Host.ConfigureContainer(builder => { try { // 注册自定义 Autofac 注册模块(批量注入服务) builder.RegisterModule(new AutofacRegister()); } catch (Exception ex) { // 捕获注册异常,拼接异常信息与内部异常,便于排查错误 throw new Exception(ex.Message + "\n" + ex.InnerException); } }); #endregion #region AutoMapper AutoMapper.IConfigurationProvider config = new MapperConfiguration(cfg => { cfg.AddProfile<_baseMappingProfile>(); }); builder.Services.AddSingleton(config); builder.Services.AddScoped(); #endregion #region DeepSeek AI 服务 // 配置HTTP客户端:DeepSeek 为长耗时调用,设置超时时间 10 分钟 builder.Services.AddHttpClient(client => client.Timeout = TimeSpan.FromMinutes(10)); #endregion #region 豆包API服务 var doubaoSetting = builder.Configuration.GetSection("DouBao").Get(); builder.Services.AddSingleton(doubaoSetting); builder.Services.AddHttpClient("Doubao", c => c.BaseAddress = new Uri(doubaoSetting.BaseAddress)); builder.Services.AddScoped(); #endregion #region 聚合API服务 builder.Services.AddControllersWithViews(); builder.Services.AddSingleton(); builder.Services.AddHttpClient("PublicJuHeApi", c => c.BaseAddress = new Uri("http://web.juhe.cn")); builder.Services.AddHttpClient("PublicJuHeTranslateApi", c => c.BaseAddress = new Uri("http://apis.juhe.cn")); #endregion #region 企业微信 API 服务 builder.Services.AddControllersWithViews(); builder.Services.AddSingleton(); builder.Services.AddHttpClient("PublicQiYeWeChatApi", c => c.BaseAddress = new Uri("https://qyapi.weixin.qq.com")); #endregion #region 混元API // 从配置文件读取腾讯云密钥信息(对应 appsettings.json 中的配置节点) var secretId = builder.Configuration["TencentCloud:SecretId"]; var secretKey = builder.Configuration["TencentCloud:SecretKey"]; var region = builder.Configuration["TencentCloud:Region"] ?? "ap-guangzhou"; // 注册 HttpClient 工厂(SDK 内部依赖使用) builder.Services.AddHttpClient(); // 注册腾讯云混元客户端(单例模式,官方推荐) builder.Services.AddSingleton(provider => { Credential cred = new Credential { SecretId = secretId, SecretKey = secretKey }; ClientProfile clientProfile = new ClientProfile(); HttpProfile httpProfile = new HttpProfile { Endpoint = "hunyuan.tencentcloudapi.com", Timeout = 60 * 10, // 超时时间:10分钟 }; clientProfile.HttpProfile = httpProfile; return new HunyuanClient(cred, region, clientProfile); }); // 注册自定义混元业务接口(作用域生命周期) builder.Services.AddScoped(); #endregion #region 有道 API 服务 //builder.Services.AddControllersWithViews(); //builder.Services.AddSingleton(); //builder.Services.AddHttpClient("PublicYouDaoApi", c => c.BaseAddress = new Uri("https://openapi.youdao.com")); #endregion #region 高德地图 API 服务 builder.Services.AddHttpClient(); #endregion #region 通用搜索服务 builder.Services.AddScoped(typeof(DynamicSearchService<>)); #endregion #region Quartz builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); //# new business builder.Services.AddControllersWithViews(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); #endregion #region SignalR builder.Services.AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.TryAddSingleton(typeof(CommonService)); #endregion #region hotmail builder.Services.AddScoped(); #endregion #region Microsoft Graph 邮件服务 builder.Services.Configure( builder.Configuration.GetSection(MicrosoftGraphMailboxOptions.SectionName)); builder.Services.AddHttpClient("MicrosoftGraph", c => { c.BaseAddress = new Uri("https://graph.microsoft.com/v1.0/"); c.Timeout = TimeSpan.FromMinutes(2); }); builder.Services.AddSingleton(); #endregion var app = builder.Build(); // Serilog日志 请求中间件 app.UseSerilogRequestLogging(options => { // 自定义日志输出模板 options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} from {ClientIP} (UA: {UserAgent}) - {StatusCode} in {Elapsed} ms"; // 自定义日志级别 options.GetLevel = (httpContext, elapsed, ex) => { // 存在异常 → 错误级别 if (ex != null) return LogEventLevel.Error; // 500+ 状态码 → 错误级别 if (httpContext.Response.StatusCode > 499) return LogEventLevel.Error; // 健康检查接口使用更低级别(Debug) if (httpContext.Request.Path.StartsWithSegments("/health")) return LogEventLevel.Debug; // 默认信息级别 return LogEventLevel.Information; }; // 丰富日志上下文(添加自定义字段) options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { // 获取客户端IP(处理代理场景) var ipAddress = CommonFun.GetClientIpAddress(httpContext); // 解析客户端操作系统 var userAgent = CommonFun.DetectOS(httpContext.Request.Headers.UserAgent.ToString()); // 添加IP及其他有用信息到日志上下文 diagnosticContext.Set("ClientIP", ipAddress); diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); diagnosticContext.Set("UserAgent", userAgent); diagnosticContext.Set("Referer", httpContext.Request.Headers.Referer.ToString()); // 对API请求额外添加请求头信息 if (httpContext.Request.Path.StartsWithSegments("/api")) { diagnosticContext.Set("RequestContentType", httpContext.Request.ContentType); diagnosticContext.Set("RequestContentLength", httpContext.Request.ContentLength ?? 0); } }; }); AutofacIocManager.Instance.Container = app.UseHostFiltering().ApplicationServices.GetAutofacRoot();//AutofacIocManager // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseCors("Cors"); //Cors //app.UseMiddleware(); // 定义允许 API 访问的时间范围 //var startTime = DateTime.Parse(_config["ApiAccessTime:StartTime"]); //var endTime = DateTime.Parse(_config["ApiAccessTime:EndTime"]); //app.UseMiddleware(startTime, endTime); // 指定 API 操作记录信息 app.UseMiddleware(); app.UseAuthentication(); // 认证授权中间件 app.UseMiddleware(); app.UseAuthorization(); // 授权 app.UseWhen(context => context.Request.Path.StartsWithSegments("/api/MarketCustomerResources/QueryNewClientData"), branch => branch.UseResponseCompression()); // 授权路由 //app.MapGet("generatetoken", c => c.Response.WriteAsync(JWTBearer.GenerateToken(c))); #region 启用SwaggerUI // 从配置读取开关,动态启用Swagger文档 if (AppSettingsHelper.Get("UseSwagger").ToBool()) { app.UseSwagger(); app.UseSwaggerUI(c => { // 默认接口文档版本 c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ver0.1"); // 遍历分组配置,动态加载多分组接口文档(上传文件分组) foreach (var item in groups) { c.SwaggerEndpoint($"/swagger/{item.Item1}/swagger.json", item.Item2); } // 设置根路径访问Swagger(直接域名打开即文档) c.RoutePrefix = string.Empty; // 默认不展开接口列表 c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None); // 隐藏模型结构,界面更简洁 c.DefaultModelsExpandDepth(-1); // 可选功能(已注释) //c.EnableFilter(); // 启用搜索功能 //c.EnableDeepLinking(); // 启用深度链接 }); } #endregion #region Quartz 定时任务 // 容器中获取 Quartz 工厂实例 var quartz = app.Services.GetRequiredService(); // 应用启动时启动 Quartz 定时任务 app.Lifetime.ApplicationStarted.Register(async () => { await quartz.Start(); }); // 应用停止时优雅关闭 Quartz 定时任务 app.Lifetime.ApplicationStopped.Register(() => { //quartz.Stop(); }); #endregion #region SignalR app.MapHub("/ChatHub", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling; }); #endregion app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();