更新框架,增加云库搜索模块

This commit is contained in:
chfei committed 2026-09-18 18:40:10 +08:00
1 parent 8526e5e0d2
commit f2caec6083
192 files changed
+3626 -804

No files matched your search

@@ -0,0 +1,27 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace MAX.Framework.DependencyInjection
{
public static class FreeSqlDB
{
public static IServiceCollection AddFreeSqlDB(this IServiceCollection services)
{
Func<IServiceProvider, IFreeSql> fsqlFactory = r =>
{
var config = r.GetRequiredService<IConfiguration>();
var connection = config.GetConnectionString("DefaultConnection");
IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.MySql, connection)
.UseAdoConnectionPool(true)
.Build();
return fsql;
};
services.AddSingleton<IFreeSql>(fsqlFactory);
return services;
}
}
}
@@ -0,0 +1,127 @@
using Asp.Versioning.OpenApi;
using MAX.Framework.Security;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi;
using System.Security.Claims;
using System.Text;
namespace MAX.Framework.DependencyInjection;
/// <summary>
/// JWT 认证服务注册入口(由宿主 Program.cs 调用)。
/// </summary>
public static class JwtServiceCollectionExtensions
{
/// <summary>
/// 注册 JWT 认证:
/// <list type="number">
/// <item>绑定 <c>Jwt</c> 配置节到 <see cref="JwtOptions"/>,并在启动时校验签名密钥长度;</item>
/// <item>注册 <see cref="ITokenService"/>(令牌生成/校验)与 <see cref="ICurrentUser"/>(当前用户访问器);</item>
/// <item>注册 <see cref="JwtBearerDefaults"/> 认证,认证失败/授权失败统一返回 <see cref="Dtos.ApiResult{T}"/> 格式;</item>
/// <item>注册 <see cref="IOnlineUserStore"/>(基于 <c>IMemoryCache</c> 的在线会话存储):单点登录、在线人员查询、退出销毁;</item>
/// <item>为 OpenAPI 文档声明 Bearer(JWT) 安全方案,并对标记了 [Authorize] 的接口声明需要携带令牌。</item>
/// </list>
/// </summary>
/// <remarks>
/// 需要在请求管道中调用 <c>app.UseAuthentication()</c>(位于 <c>app.UseAuthorization()</c> 之前)。
/// </remarks>
public static IServiceCollection AddStoreHubJwt(this IServiceCollection services, IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
// JwtOptions:绑定 Jwt 配置节,启动时校验 HS256 签名密钥长度
services.AddOptions<JwtOptions>()
.Bind(configuration.GetSection(JwtOptions.SectionName))
.Validate(
o => !string.IsNullOrWhiteSpace(o.SigningKey)
&& Encoding.UTF8.GetByteCount(o.SigningKey) >= 32,
"Jwt:SigningKey 长度不足,HS256 签名密钥至少需要 32 字节(256位)。")
.ValidateOnStart();
services.AddHttpContextAccessor();
// 令牌服务与当前用户访问器
services.AddSingleton<ITokenService, TokenService>();
services.AddScoped<ICurrentUser, CurrentUser>();
// 在线会话存储(单点登录):基于 IMemoryCache 的进程内缓存,用于在线人员管理与退出销毁
services.AddMemoryCache();
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
// JwtBearer 认证
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
JwtOptions jwt = configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>() ?? new JwtOptions();
options.TokenValidationParameters = jwt.ToTokenValidationParameters();
options.Events = new JwtBearerEvents
{
OnChallenge = JwtBearerResponseHandler.OnChallenge,
OnForbidden = JwtBearerResponseHandler.OnForbidden,
// 单点登录校验:会话必须仍有效(未退出、未被新登录顶替)
OnTokenValidated = context =>
{
IOnlineUserStore onlineUserStore = context.HttpContext.RequestServices.GetRequiredService<IOnlineUserStore>();
ClaimsPrincipal? principal = context.Principal;
string? userName = principal?.FindFirst(JwtClaimTypes.UserName)?.Value;
string? jti = principal?.FindFirst(JwtClaimTypes.Jti)?.Value;
if (string.IsNullOrWhiteSpace(userName)
|| string.IsNullOrWhiteSpace(jti)
|| !onlineUserStore.IsTokenActive(userName, jti))
{
context.Fail(new SecurityTokenException("登录已失效,请重新登录"));
}
return Task.CompletedTask;
},
};
});
// OpenAPI:声明 Bearer(JWT) 安全方案(Scalar 页面即可显示 Authorize/输入 token),
// 并仅对标记了 [Authorize] 的接口声明需要携带令牌(与宿主的 AddOpenApi 配置合并生效)
services.Configure<VersionedOpenApiOptions>(options =>
{
// 在 OpenAPI 文档中声明 Bearer(JWT) 安全方案
options.Document.AddDocumentTransformer((document, _, _) =>
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes.TryAdd("Bearer", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "登录接口返回的 accessToken,直接粘贴令牌内容即可(无需手动加 Bearer 前缀)。",
});
return Task.CompletedTask;
});
// 仅对标记了 [Authorize] 的接口声明需要携带令牌
options.Document.AddOperationTransformer((operation, context, _) =>
{
IList<object> metadata = context.Description.ActionDescriptor.EndpointMetadata;
bool requiresAuth = metadata.OfType<IAuthorizeData>().Any()
&& !metadata.OfType<IAllowAnonymous>().Any();
if (requiresAuth)
{
operation.Security =
[
new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", context.Document, "")] = [],
},
];
}
return Task.CompletedTask;
});
});
return services;
}
}
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Web;
using System;
using System.Collections.Generic;
using System.Text;
namespace MAX.Framework.DependencyInjection
{
public static class LoggerExtensions
{
public static WebApplicationBuilder AddLogger(this WebApplicationBuilder builder)
{
builder.Logging.ClearProviders();
//builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.Host.UseNLog();
return builder;
}
}
}
@@ -0,0 +1,163 @@
using MAX.Core.DependencyInjection;
using MAX.Core.Interfaces;
using MAX.Framework.Commons;
using MAX.Framework.Dtos;
using MAX.Framework.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
using System.Reflection;
namespace MAX.Framework.DependencyInjection
{
/// <summary>
/// IOC 注册入口(由宿主 Program.cs 调用)
/// </summary>
public static class ServiceCollectionExtensions
{
private static readonly Type[] DependencyMarkers =
[
typeof(ISingletonDependency),
typeof(IScopedDependency),
typeof(ITransientDependency),
];
/// <summary>
/// 注册应用服务:
/// <list type="number">
/// <item>自动注册所有实现 <see cref="ISingletonDependency"/> / <see cref="IScopedDependency"/> / <see cref="ITransientDependency"/> 的类型;</item>
/// <item>发现所有 <see cref="IStartup"/>,按 <see cref="IStartup.Priority"/> 降序执行 Initialize + Register。</item>
/// </list>
/// </summary>
public static IServiceCollection AddStoreHub(this IServiceCollection services)
{
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
RegisterDependencyMarkers(services, assemblies);
RunStartups(services, assemblies);
RunModules(services, assemblies);
services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
options.Filters.Add<UnifyResultFilter>();
}).ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
// 取出所有校验错误信息,拼成一个字符串(或者只取第一个)
var errors = context.ModelState
.Where(e => e.Value?.Errors.Count > 0)
.SelectMany(e => e.Value!.Errors.Select(err => err.ErrorMessage))
.ToList();
var msg = string.Join("; ", errors);
var result = ApiResult.Fail(msg, ResultCode.ValidationError);
return new ObjectResult(result)
{
StatusCode = 200 // 和其他接口保持一致,用Code表达失败,而不是HTTP 400
};
};
});
return services;
}
/// <summary>
/// 自动注册标记接口(ISingletonDependency / IScopedDependency / ITransientDependency)的实现类型
/// </summary>
private static void RegisterDependencyMarkers(IServiceCollection services, IEnumerable<Assembly> assemblies)
{
foreach (Type type in GetConcreteTypes(assemblies))
{
Debug.WriteLine(type.Namespace);
Type? marker = DependencyMarkers.FirstOrDefault(m => m.IsAssignableFrom(type));
if (marker is null)
{
continue;
}
ServiceLifetime lifetime = marker == typeof(ISingletonDependency) ? ServiceLifetime.Singleton
: marker == typeof(IScopedDependency) ? ServiceLifetime.Scoped
: ServiceLifetime.Transient;
// 注册为自身
services.Add(new ServiceDescriptor(type, type, lifetime));
// 注册为实现的接口(排除标记接口与系统接口)
foreach (Type iface in type.GetInterfaces())
{
if (DependencyMarkers.Contains(iface) || IsSystemInterface(iface))
{
continue;
}
services.Add(new ServiceDescriptor(iface, type, lifetime));
}
}
}
/// <summary>
/// 发现所有 <see cref="IStartup"/> 实现,按 <see cref="IStartup.Priority"/> 降序执行
/// </summary>
private static void RunStartups(IServiceCollection services, IEnumerable<Assembly> assemblies)
{
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
foreach (IStartup startup in GetConcreteTypes(assemblies)
.Where(t => typeof(IStartup).IsAssignableFrom(t))
.Select(t => (IStartup)Activator.CreateInstance(t, true)!)
.OrderByDescending(s => s.Priority))
{
startup.Initialize();
startup.Register(registrar);
}
}
private static void RunModules(IServiceCollection services, IEnumerable<Assembly> assemblies)
{
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
foreach (IModule startup in GetConcreteTypes(assemblies)
.Where(t => typeof(IModule).IsAssignableFrom(t))
.Select(t => (IModule)Activator.CreateInstance(t, true)!))
{
startup.AddModule(services);
}
}
private static IEnumerable<Type> GetConcreteTypes(IEnumerable<Assembly> assemblies)
{
return assemblies
.SelectMany(SafeGetTypes)
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericTypeDefinition);
}
private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.OfType<Type>();
}
}
private static IEnumerable<Assembly> GetApplicationAssemblies()
{
// 使用 AppTypeFinder:扫描应用输出目录 + 已加载程序集,
return StoreHubAppTypeFinder.Current.GetAssemblies();
}
private static bool IsSystemInterface(Type type)
{
return type.Namespace is null
|| type.Namespace.StartsWith("System", StringComparison.Ordinal)
|| type.Namespace.StartsWith("Microsoft", StringComparison.Ordinal);
}
}
}
@@ -0,0 +1,88 @@
using MAX.Core.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
namespace MAX.Framework.DependencyInjection
{
/// <summary>
/// 将 <see cref="IServiceRegistrar"/> 适配到 Microsoft.Extensions.DependencyInjection
/// </summary>
internal sealed class ServiceRegistrarProxy : IServiceRegistrar
{
private readonly IServiceCollection _services;
public ServiceRegistrarProxy(IServiceCollection services)
{
_services = services;
}
public void RegisterSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService
{
_services.AddSingleton<TService, TImplementation>();
}
public void RegisterSingleton<TService>(TService instance) where TService : class
{
_services.AddSingleton(instance);
}
public void RegisterSingleton<TService>(Func<IServiceProvider, TService> factory) where TService : class
{
_services.AddSingleton(factory);
}
public void RegisterSingleton<TService, TImplementation>(object? serviceKey) where TService : class where TImplementation : class, TService
{
_services.AddKeyedSingleton<TService, TImplementation>(serviceKey);
}
public void RegisterSingleton<TService>(object? serviceKey, TService instance) where TService : class
{
_services.AddKeyedSingleton(serviceKey, instance);
}
public void RegisterSingleton<TService>(object? serviceKey, Func<IServiceProvider, object?, TService> factory) where TService : class
{
_services.AddKeyedSingleton(serviceKey, factory);
}
public void RegisterScoped<TService, TImplementation>() where TService : class where TImplementation : class, TService
{
_services.AddScoped<TService, TImplementation>();
}
public void RegisterScoped<TService>(Func<IServiceProvider, TService> factory) where TService : class
{
_services.AddScoped(factory);
}
public void RegisterScoped<TService, TImplementation>(object? serviceKey) where TService : class where TImplementation : class, TService
{
_services.AddKeyedScoped<TService, TImplementation>(serviceKey);
}
public void RegisterScoped<TService>(object? serviceKey, Func<IServiceProvider, object?, TService> factory) where TService : class
{
_services.AddKeyedScoped(serviceKey, factory);
}
public void RegisterTransient<TService, TImplementation>() where TService : class where TImplementation : class, TService
{
_services.AddTransient<TService, TImplementation>();
}
public void RegisterTransient<TService>(Func<IServiceProvider, TService> factory) where TService : class
{
_services.AddTransient(factory);
}
public void RegisterTransient<TService, TImplementation>(object? serviceKey) where TService : class where TImplementation : class, TService
{
_services.AddKeyedTransient<TService, TImplementation>(serviceKey);
}
public void RegisterTransient<TService>(object? serviceKey, Func<IServiceProvider, object?, TService> factory) where TService : class
{
_services.AddKeyedTransient(serviceKey, factory);
}
}
}
@@ -0,0 +1,18 @@
using MAX.Core.Reflections;
namespace MAX.Framework.DependencyInjection
{
/// <summary>
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
/// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。
/// </summary>
internal sealed class StoreHubAppTypeFinder : AppTypeFinder
{
public static StoreHubAppTypeFinder Current { get; } = new();
protected override List<string> AvailablePrefix()
{
return ["Store"];
}
}
}