Files
douyin/extension/ServiceExtension.cs
T
2026-01-26 18:29:19 +08:00

584 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using dy.net.job;
using dy.net.service;
using dy.net.utils;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.IdentityModel.Tokens;
//using Microsoft.OpenApi.Models;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using Serilog;
using Serilog.Events;
using Serilog.Filters;
using Serilog.Formatting.Compact;
//using Serilog.Formatting.Compact;
using SqlSugar;
//using Swashbuckle.AspNetCore.SwaggerGen;
//using Swashbuckle.AspNetCore.SwaggerUI;
using System.IO.Compression;
using System.Net.Security;
using System.Reflection;
using System.Text;
namespace dy.net.extension
{
public static class ServiceExtension
{
public class SwaggerOptions
{
public string Title { get; set; }
}
public static string FnDataFolder = string.Empty;
private static DbType GetDBType(IConfiguration configuration)
{
DbType dbType = DbType.Sqlite;
var dbtypeString = configuration["dbtype"].ToLower();
// 获取颜色枚举类型的所有枚举值
var dbtypes = Enum.GetValues(typeof(DbType));
foreach (DbType type in dbtypes)
{
if (type.ToString().ToLower() == dbtypeString)
{
dbType = type;
break;
}
}
return dbType;
}
//private static string GetConnString(IConfiguration configuration, DbType dbType)
//{
// //var connectionString = configuration["dbconn"];
// if (dbType == DbType.Sqlite)
// {
// connectionString = CreateSqliteDBConn();
// }
// return connectionString;
//}
static string CreateSqliteDBConn(string dbPath = "")
{
string fileFloder = Path.Combine(Environment.CurrentDirectory, "db");
if (!string.IsNullOrEmpty(dbPath))
{
fileFloder = Path.Combine(dbPath, "db");
FnDataFolder = Path.Combine(dbPath, "mp3");
if ((!Directory.Exists(FnDataFolder)))
{
Directory.CreateDirectory(FnDataFolder);
}
}
else
{
if (Appsettings.Get("deploy") == "fn")
{
Log.Error($"fn--dbpath,未正常获取到,请进Q群联系作者 759876963");
throw new Exception("fn--dbpath,未正常获取到,请进Q群联系作者 759876963");
}
}
if (!Directory.Exists(fileFloder))
{
Directory.CreateDirectory(fileFloder);
}
var filePath = Path.Combine(fileFloder, "dy.sqlite");
string conn = $"DataSource={filePath}";
if (!File.Exists(filePath))
{
File.Create(filePath).Close();
}
return conn;
}
/// <summary>
/// 配置JWT认证
/// </summary>
public static void ConfigureJwtAuthentication(this IServiceCollection services)
{
var key = Encoding.ASCII.GetBytes(Md5Util.JWT_TOKEN_KEY);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Headers["Authorization"]
.FirstOrDefault()?.Split(" ").Last();
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(60)
};
});
}
public static void AddSqlsugar(this IServiceCollection services, string dbpath)
{
//DbType dbtype = GetDBType(configuration);
services.AddScoped<ISqlSugarClient>(db =>
{
var sqlSugar = new SqlSugarClient(new ConnectionConfig
{
ConnectionString = CreateSqliteDBConn(dbpath),
InitKeyType = InitKeyType.Attribute,
DbType = DbType.Sqlite,
IsAutoCloseConnection = true // close connection after each operation (recommended)
}, db =>
{
//单例参数配置,所有上下文生效
db.Aop.OnLogExecuting = (sql, pars) =>
{
//Serilog.Log.Debug(sql);//输出sql
};
db.Aop.OnError = (e) =>
{
Serilog.Log.Error(e.Message);
Serilog.Log.Error(e.Sql);
};
db.DbMaintenance.CreateDatabase(); // 检查数据库是否存在,如果不存在则创建
Assembly assembly = Assembly.GetExecutingAssembly(); // 获取当前程序集
Type[] types = assembly.GetTypes(); // 获取程序集中的所有类型
var targetClasses = types
.Where(t => t.Namespace != null && t.Namespace.StartsWith("dy.net.model.entity"))
.ToList();
db.CodeFirst.InitTables(targetClasses.ToArray());
});
return sqlSugar;
});
}
/// <summary>
///
/// </summary>
/// <param name="services"></param>
public static void AddQuartzService(this IServiceCollection services,string dbPath)
{
// 1. 注册所有 Job 到 DI 容器,推荐使用 Scoped(最符合 Job 执行特性)
services.AddScoped<DouyinCollectSyncJob>();
services.AddScoped<DouyinFavoritSyncJob>();
services.AddScoped<DouyinFollowedSyncJob>();
services.AddScoped<DouyinFollowsAndCollnectsSyncJob>();
services.AddScoped<DouyinCollectCustomSyncJob>();
services.AddScoped<DouyinMixSyncJob>();
services.AddScoped<DouyinSeriesSyncJob>();
// 3. 配置 Quartz
services.AddQuartz(q =>
{
q.SchedulerId = "DouyinQuartzScheduler";
q.SchedulerName = "DouyinSyncScheduler";
q.InterruptJobsOnShutdownWithWait = false;
q.UseDedicatedThreadPool(5); // 建议增加线程数,1个太少容易阻塞
q.MisfireThreshold = TimeSpan.FromMinutes(2);
q.UsePersistentStore(s =>
{
s.UseSQLite(config =>
{
config.ConnectionString = CreateSqliteDBConn(dbPath);
config.TablePrefix = "QRTZ_";
});
s.UseProperties = false;
s.UseBinarySerializer();
});
});
services.AddQuartzHostedService(q =>
{
q.WaitForJobsToComplete = true;
q.AwaitApplicationStarted = true;
});
services.AddTransient<DouyinQuartzJobService>();
}
public static void AddHttpClients(this IServiceCollection services)
{
// 通用的忽略SSL证书验证的HttpMessageHandler配置
static HttpMessageHandler ignoreSslHandlerFactory()
{
var handler = new SocketsHttpHandler
{
// 控制并发连接数(根据服务器承受能力调整,建议5-20)
MaxConnectionsPerServer = 5,
// 禁用代理自动检测(减少不必要的延迟)
UseProxy = false,
// 连接超时(建立连接的超时时间)
ConnectTimeout = Timeout.InfiniteTimeSpan,// TimeSpan.FromSeconds(120),
// 忽略HTTPS证书验证
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
}
};
return handler;
}
// 抖音数据接口请求客户端
services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT, client =>
{
client.DefaultRequestHeaders.Add("User-Agent", DouyinRequestParamManager.DY_USER_AGENT);
client.BaseAddress = new Uri("https://www.douyin.com");
}).ConfigurePrimaryHttpMessageHandler(ignoreSslHandlerFactory);
// 抖音下载客户端(单独配置,保持原有特性)
services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT_DOWN)
.ConfigurePrimaryHttpMessageHandler(ignoreSslHandlerFactory)
.ConfigureHttpClient(client =>
{
client.DefaultRequestHeaders.Add("User-Agent", DouyinRequestParamManager.DY_USER_AGENT);
client.DefaultRequestHeaders.Add("Referer", "https://www.douyin.com");
});
}
/// <summary>
/// 自动注入指定命名空间下的所有服务
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="namespace">目标命名空间(如 "MyApp.Services"</param>
/// <param name="assembly">服务所在的程序集(默认当前程序集)</param>
/// <param name="includeSubNamespaces">是否包含子命名空间</param>
public static IServiceCollection AddServicesFromNamespace(
this IServiceCollection services,
string @namespace,
Assembly? assembly = null,
bool includeSubNamespaces = false)
{
// 默认使用调用方的程序集
assembly ??= Assembly.GetCallingAssembly();
// 扫描程序集中指定命名空间的所有类
var types = assembly.GetTypes()
.Where(type =>
type.IsClass && // 只处理类
!type.IsAbstract && // 排除抽象类
!type.IsGenericTypeDefinition && // 排除泛型类
(includeSubNamespaces
? type.Namespace?.StartsWith(@namespace, StringComparison.Ordinal) == true
: type.Namespace == @namespace) // 匹配命名空间(含子命名空间或精确匹配)
);
foreach (var type in types)
{
// 获取服务的生命周期(优先使用特性,默认 Transient)
var lifetime = type.GetCustomAttribute<ServiceLifetimeAttribute>()?.Lifetime
?? ServiceLifetime.Scoped;
// 查找该类实现的接口(优先注册为接口服务)
//var interfaces = type.GetInterfaces()
// .Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
// .ToList();
// 查找该类实现的接口(排除 IDisposable
var interfaces = type.GetInterfaces()
.Where(i => i != typeof(IDisposable)) // <-- 关键修正
.ToList();
if (interfaces.Any())
{
// 注册:接口 -> 实现类(如 IUserService -> UserService
foreach (var @interface in interfaces)
{
services.Add(new ServiceDescriptor(@interface, type, lifetime));
}
}
else
{
// 无接口时,直接注册类本身(如 UserService 注册为自身)
services.Add(new ServiceDescriptor(type, type, lifetime));
}
}
return services;
}
///// <summary>
///// SwaggerUi
///// </summary>
///// <param name="app"></param>
///// <param name="options"></param>
//public static void UseCustomSwaggerUI(this IApplicationBuilder app, Action<SwaggerOptions> options)
//{
// SwaggerOptions option = new SwaggerOptions();
// options?.Invoke(option);
// //启用中间件服务生成Swagger作为JSON终结点
// app.UseSwagger(c =>
// {
// //c.SerializeAsV2 = true;
// //c.RouteTemplate = "api-docs/{documentName}/swagger.json";
// c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
// {
// swaggerDoc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}" } };
// OpenApiPaths paths = new OpenApiPaths();
// foreach (var path in swaggerDoc.Paths)
// {
// //if ( path.Key.StartsWith("/v1/api") )//做版本控制
// paths.Add(path.Key, path.Value);
// }
// swaggerDoc.Paths = paths;
// });
// });
// //启用中间件服务对swagger-ui,指定Swagger JSON终结点
// app.UseSwaggerUI(c =>
// {
// //c.MaxDisplayedTags(5);
// //c.DisplayOperationId();//唯一标识操作
// c.SwaggerEndpoint("/swagger/v1/swagger.json", option.Title);
// //c.SwaggerEndpoint("/swagger/v2/swagger.json", "V2 Docs");
// c.RoutePrefix = "swagger";//根路由
// c.EnableDeepLinking();//启用深度链接--不知道干嘛的
// c.DisplayRequestDuration();//调试,显示接口响应时间
// c.EnableValidator();//验证
// c.DocExpansion(DocExpansion.List);//默认展开
// c.DefaultModelsExpandDepth(-1);//隐藏model
// c.DefaultModelExpandDepth(3);//model展开层级
// c.EnableFilter();//筛选--如果接口过多可以开启
// c.DefaultModelRendering(ModelRendering.Model);//设置显示参数的实体或Example
// //c.SupportedSubmitMethods(SubmitMethod.Get , SubmitMethod.Head , SubmitMethod.Post);//
// //c.OAuthClientId("test-id");
// //c.OAuthClientSecret("test-secret");
// //c.OAuthRealm("test-realm");
// //c.OAuthAppName("test-app");
// //c.OAuthScopeSeparator(" ");
// //c.OAuthAdditionalQueryStringParams(new Dictionary<string, string> { { "foo", "bar" } });
// //c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
// });
//}
///// <summary>
///// Swagger
///// </summary>
///// <param name="services"></param>
//public static IServiceCollection AddSwagger(this IServiceCollection services, Action<SwaggerGenOptions> options = null)
//{
// if (options != null)
// services.AddSwaggerGen(options);
// else
// services.AddSwaggerGen(DefaultSwaggerGenOptions());
// return services;
//}
//private static Action<SwaggerGenOptions> DefaultSwaggerGenOptions()
//{
// Action<SwaggerGenOptions> options = o =>
// {
// o.OperationFilter<SwaggerAuthorizationFilter>();
// o.SwaggerDoc("v1", new OpenApiInfo
// {
// Version = "v1",
// Title = "dy.net API Swagger Document",
// });
// o.OrderActionsBy((apiDesc) => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.HttpMethod}");
// o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
// {
// Description = "请在下方输入:Bearer {Token}",
// Name = "Authorization",
// In = ParameterLocation.Header,
// Type = SecuritySchemeType.ApiKey,
// BearerFormat = "JWT",
// Scheme = "Bearer",
// });
// o.AddSecurityRequirement(new OpenApiSecurityRequirement
// {
// {
// new OpenApiSecurityScheme
// {
// Reference = new OpenApiReference {
// Type = ReferenceType.SecurityScheme,
// Id = "Bearer",
// }
// },
// new[] { "readAccess", "writeAccess" }
// }
// });
// o.DocumentFilter<SwaggerHiddenApiFilter>();
// var XmlPath = $"{AppContext.BaseDirectory}{AppDomain.CurrentDomain.FriendlyName}.xml";
// o.IncludeXmlComments(XmlPath);
// o.EnableAnnotations();
// };
// return options;
//}
/// <summary>
/// Serilog 日志拓展
/// </summary>
public static void ConfigureLogging(this WebApplicationBuilder builder)
{
builder.Host.ConfigureLogging(logging => logging.ClearProviders())
.UseSerilog();
string dateFile = "";// DateTime.Now.ToString("yyyyMMdd");
Log.Logger = new LoggerConfiguration()
//.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Is(LogEventLevel.Debug)
.Enrich.FromLogContext()
.Filter.ByExcluding(e => e.Level == LogEventLevel.Information) // 排除Info级别的日志
.Filter.ByExcluding(Matching.FromSource("Microsoft"))
.Filter.ByExcluding(Matching.FromSource("Quartz"))
.WriteTo.Console(new RenderedCompactJsonFormatter(), LogEventLevel.Debug)
//.WriteTo.MySQL(connectionString: builder.Configuration.GetConnectionString("DbConnectionString"), tableName: "Logs") // 输出到数据库
.WriteTo.Logger(configure => configure
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Debug)
.WriteTo.File(
$"logs/log-debug-{dateFile}.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"))
//.WriteTo.Logger(configure => configure
// .Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Information)
// .WriteTo.File(
// $"logs/log-info-{dateFile}.txt",
// rollingInterval: RollingInterval.Day,
// outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"))
.WriteTo.Logger(configure => configure
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Error)
.WriteTo.File(
$"logs/log-error-{dateFile}.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"))
//.WriteTo.File(
// $"logs/log-total-{dateFile}.txt",
// rollingInterval: RollingInterval.Day,
// outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
// restrictedToMinimumLevel: LogEventLevel.Debug)
.CreateLogger();
}
/// <summary>
/// 响应压缩
///
/// services.AddMyResponseCompression(); 需要配合 app.UseResponseCompression();
/// </summary>
/// <param name="services"></param>
public static void AddMyResponseCompression(this IServiceCollection services)
{
// 第一步: 配置gzip与br的压缩等级为最优
services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
// 第二步: 添加中间件
services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
// 添加br与gzip的Provider
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
// 扩展一些类型 (MimeTypes中有一些基本的类型,可以打断点看看)
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
{
"text/html; charset=utf-8",
"application/xhtml+xml",
"application/atom+xml",
"image/svg+xml",
"application/octet-stream"
});
});
}
}
//public class SwaggerAuthorizationFilter : IOperationFilter
//{
// public void Apply(OpenApiOperation operation, OperationFilterContext context)
// {
// operation.Parameters ??= new List<OpenApiParameter>();
// _ = context.ApiDescription.ActionDescriptor.AttributeRouteInfo;
// //先判断是否是匿名访问,
// if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor descriptor)
// {
// var Authorizes = descriptor.MethodInfo.GetCustomAttributes(typeof(AuthorizeFilter), true);
// //非匿名的方法,链接中添加accesstoken值
// if (Authorizes.Any())
// {
// operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
// //operation.Parameters.Add(new OpenApiParameter()
// //{
// // Required = true,
// // Name = "Bearer",
// // In = ParameterLocation.Header,
// // Description = "You Must Request With token",
// // Style = ParameterStyle.DeepObject,
// //});
// }
// }
// }
//}
/// <summary>
///
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public partial class HiddenApiAttribute : Attribute { }
/// <summary>
///
/// </summary>
//public class SwaggerHiddenApiFilter : IDocumentFilter
//{
// public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
// {
// foreach (ApiDescription apiDescription in context.ApiDescriptions)
// {
// if (apiDescription.TryGetMethodInfo(out MethodInfo method))
// {
// if (method.ReflectedType.CustomAttributes.Any(t => t.AttributeType == typeof(HiddenApiAttribute))
// || method.CustomAttributes.Any(t => t.AttributeType == typeof(HiddenApiAttribute)))
// {
// string key = "/" + apiDescription.RelativePath;
// if (key.Contains("?"))
// {
// int idx = key.IndexOf("?", StringComparison.Ordinal);
// key = key.Substring(0, idx);
// }
// swaggerDoc.Paths.Remove(key);
// }
// }
// }
// }
//}
}