mirror of
				https://github.com/nsnail/NetAdmin.git
				synced 2025-10-31 19:35:26 +08:00 
			
		
		
		
	Tk (#197)
* refactor: ♻️ 业务代码项目文件名与框架代码项目文件名区分 * refactor: ♻️ 业务代码项目文件名与框架代码项目文件名区分 --------- Co-authored-by: tk <fiyne1a@dingtalk.com>
This commit is contained in:
		| @@ -0,0 +1,31 @@ | |||||||
|  | namespace NetAdmin.Application.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     工作单元管理器扩展方法 | ||||||
|  | /// </summary> | ||||||
|  | public static class UnitOfWorkManagerExtensions | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     事务操作 | ||||||
|  |     /// </summary> | ||||||
|  |     public static async Task AtomicOperateAsync(this UnitOfWorkManager me, Func<Task> handle) | ||||||
|  |     { | ||||||
|  |         var       logger     = LogHelper.Get<UnitOfWorkManager>(); | ||||||
|  |         using var unitOfWork = me.Begin(); | ||||||
|  |         var       hashCode   = unitOfWork.GetHashCode(); | ||||||
|  |         try { | ||||||
|  |             #if DEBUG | ||||||
|  |             logger?.Debug($"{Ln.开始事务}: {hashCode}"); | ||||||
|  |             #endif | ||||||
|  |             await handle().ConfigureAwait(false); | ||||||
|  |             unitOfWork.Commit(); | ||||||
|  |             logger?.Info($"{Ln.事务已提交}: {hashCode}"); | ||||||
|  |         } | ||||||
|  |         catch (Exception ex) { | ||||||
|  |             logger?.Warn(ex); | ||||||
|  |             unitOfWork.Rollback(); | ||||||
|  |             logger?.Warn($"{Ln.事务已回滚}: {hashCode}"); | ||||||
|  |             throw; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | using NetAdmin.Domain; | ||||||
|  | using NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Modules; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     增删改查模块接口 | ||||||
|  | /// </summary> | ||||||
|  | /// <typeparam name="TCreateReq">创建请求类型</typeparam> | ||||||
|  | /// <typeparam name="TCreateRsp">创建响应类型</typeparam> | ||||||
|  | /// <typeparam name="TQueryReq">查询请求类型</typeparam> | ||||||
|  | /// <typeparam name="TQueryRsp">查询响应类型</typeparam> | ||||||
|  | /// <typeparam name="TDelReq">删除请求类型</typeparam> | ||||||
|  | public interface ICrudModule<in TCreateReq, TCreateRsp, TQueryReq, TQueryRsp, TDelReq> | ||||||
|  |     where TCreateReq : DataAbstraction, new() | ||||||
|  |     where TCreateRsp : DataAbstraction | ||||||
|  |     where TQueryReq : DataAbstraction, new() | ||||||
|  |     where TQueryRsp : DataAbstraction | ||||||
|  |     where TDelReq : DataAbstraction, new() | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     批量删除实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<int> BulkDeleteAsync(BulkReq<TDelReq> req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     实体计数 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<long> CountAsync(QueryReq<TQueryReq> req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<TCreateRsp> CreateAsync(TCreateReq req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     删除实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<int> DeleteAsync(TDelReq req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     判断实体是否存在 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<bool> ExistAsync(QueryReq<TQueryReq> req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     导出实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<IActionResult> ExportAsync(QueryReq<TQueryReq> req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取单个实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<TQueryRsp> GetAsync(TQueryReq req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     分页查询实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<PagedQueryRsp<TQueryRsp>> PagedQueryAsync(PagedQueryReq<TQueryReq> req); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     查询实体 | ||||||
|  |     /// </summary> | ||||||
|  |     Task<IEnumerable<TQueryRsp>> QueryAsync(QueryReq<TQueryReq> req); | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | using NetAdmin.Domain.Dto.Dependency; | ||||||
|  | using NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Modules.Tpl; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     示例模块 | ||||||
|  | /// </summary> | ||||||
|  | public interface IExampleModule : ICrudModule<CreateExampleReq, QueryExampleRsp // 创建类型 | ||||||
|  |   , QueryExampleReq, QueryExampleRsp                                            // 查询类型 | ||||||
|  |   , DelReq                                                                      // 删除类型 | ||||||
|  | >; | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |     <Import Project="$(SolutionDir)/build/code.quality.props"/> | ||||||
|  |     <ItemGroup> | ||||||
|  |         <ProjectReference Include="../NetAdmin.Domain/NetAdmin.Domain.csproj"/> | ||||||
|  |     </ItemGroup> | ||||||
|  | </Project> | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | using NetAdmin.Domain.Contexts; | ||||||
|  | using NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Repositories; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     基础仓储 | ||||||
|  | /// </summary> | ||||||
|  | public sealed class BasicRepository<TEntity, TPrimary>(IFreeSql fSql, UnitOfWorkManager uowManger, ContextUserToken userToken) | ||||||
|  |     : DefaultRepository<TEntity, TPrimary>(fSql, uowManger) | ||||||
|  |     where TEntity : EntityBase<TPrimary> // | ||||||
|  |     where TPrimary : IEquatable<TPrimary> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     当前上下文关联的用户令牌 | ||||||
|  |     /// </summary> | ||||||
|  |     public ContextUserToken UserToken => userToken; | ||||||
|  | } | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | using NetAdmin.Domain.Contexts; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     服务接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IService | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     服务编号 | ||||||
|  |     /// </summary> | ||||||
|  |     Guid ServiceId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     上下文用户令牌 | ||||||
|  |     /// </summary> | ||||||
|  |     ContextUserToken UserToken { get; set; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | using NetAdmin.Application.Repositories; | ||||||
|  | using NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  | using StackExchange.Redis; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Redis Service Base | ||||||
|  | /// </summary> | ||||||
|  | /// <remarks> | ||||||
|  | ///     Initializes a new instance of the <see cref="RedisService{TEntity, TPrimary, TLogger}" /> class. | ||||||
|  | ///     Redis Service Base | ||||||
|  | /// </remarks> | ||||||
|  | public abstract class RedisService<TEntity, TPrimary, TLogger>(BasicRepository<TEntity, TPrimary> rpo) | ||||||
|  |     : RepositoryService<TEntity, TPrimary, TLogger>(rpo) | ||||||
|  |     where TEntity : EntityBase<TPrimary> // | ||||||
|  |     where TPrimary : IEquatable<TPrimary> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Redis Database | ||||||
|  |     /// </summary> | ||||||
|  |     protected IDatabase RedisDatabase { get; } // | ||||||
|  |         = App.GetService<IConnectionMultiplexer>() | ||||||
|  |              .GetDatabase(App.GetOptions<RedisOptions>().Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE).Database); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取锁 | ||||||
|  |     /// </summary> | ||||||
|  |     protected Task<RedisLocker> GetLockerAsync(string lockerName) | ||||||
|  |     { | ||||||
|  |         return RedisLocker.GetLockerAsync(RedisDatabase, lockerName, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY) | ||||||
|  |                                         , Numbers.MAX_LIMIT_RETRY_CNT_REDIS_LOCK, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取锁(仅获取一次) | ||||||
|  |     /// </summary> | ||||||
|  |     protected Task<RedisLocker> GetLockerOnceAsync(string lockerName) | ||||||
|  |     { | ||||||
|  |         return RedisLocker.GetLockerAsync(RedisDatabase, lockerName, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), 1 | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,159 @@ | |||||||
|  | using CsvHelper; | ||||||
|  | using Microsoft.Net.Http.Headers; | ||||||
|  | using NetAdmin.Application.Repositories; | ||||||
|  | using NetAdmin.Domain; | ||||||
|  | using NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  | using NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  | using NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     仓储服务基类 | ||||||
|  | /// </summary> | ||||||
|  | /// <typeparam name="TEntity">实体类型</typeparam> | ||||||
|  | /// <typeparam name="TPrimary">主键类型</typeparam> | ||||||
|  | /// <typeparam name="TLogger">日志类型</typeparam> | ||||||
|  | public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicRepository<TEntity, TPrimary> rpo) : ServiceBase<TLogger> | ||||||
|  |     where TEntity : EntityBase<TPrimary> // | ||||||
|  |     where TPrimary : IEquatable<TPrimary> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     默认仓储 | ||||||
|  |     /// </summary> | ||||||
|  |     protected BasicRepository<TEntity, TPrimary> Rpo => rpo; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     启用级联保存 | ||||||
|  |     /// </summary> | ||||||
|  |     protected bool EnableCascadeSave { | ||||||
|  |         get => Rpo.DbContextOptions.EnableCascadeSave; | ||||||
|  |         set => Rpo.DbContextOptions.EnableCascadeSave = value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     导出实体 | ||||||
|  |     /// </summary> | ||||||
|  |     protected static async Task<IActionResult> ExportAsync<TQuery, TExport>( // | ||||||
|  |         Func<QueryReq<TQuery>, ISelectGrouping<TEntity, TEntity>>            selector, QueryReq<TQuery> query, string fileName | ||||||
|  |       , Expression<Func<ISelectGroupingAggregate<TEntity, TEntity>, object>> listExp = null) | ||||||
|  |         where TQuery : DataAbstraction, new() | ||||||
|  |     { | ||||||
|  |         var list = await selector(query).Take(Numbers.MAX_LIMIT_EXPORT).ToListAsync(listExp).ConfigureAwait(false); | ||||||
|  |         return await GetExportFileStreamAsync<TExport>(fileName, list).ConfigureAwait(false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     导出实体 | ||||||
|  |     /// </summary> | ||||||
|  |     protected static async Task<IActionResult> ExportAsync<TQuery, TExport>( // | ||||||
|  |         Func<QueryReq<TQuery>, ISelect<TEntity>> selector, QueryReq<TQuery> query, string fileName, Expression<Func<TEntity, object>> listExp = null) | ||||||
|  |         where TQuery : DataAbstraction, new() | ||||||
|  |     { | ||||||
|  |         var select = selector(query) | ||||||
|  |             #if DBTYPE_SQLSERVER | ||||||
|  |                      .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) | ||||||
|  |             #endif | ||||||
|  |             .Take(Numbers.MAX_LIMIT_EXPORT); | ||||||
|  |  | ||||||
|  |         object list = listExp == null ? await select.ToListAsync().ConfigureAwait(false) : await select.ToListAsync(listExp).ConfigureAwait(false); | ||||||
|  |  | ||||||
|  |         return await GetExportFileStreamAsync<TExport>(fileName, list).ConfigureAwait(false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     更新实体 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="newValue">新的值</param> | ||||||
|  |     /// <param name="includeFields">包含的属性</param> | ||||||
|  |     /// <param name="excludeFields">排除的属性</param> | ||||||
|  |     /// <param name="whereExp">查询表达式</param> | ||||||
|  |     /// <param name="whereSql">查询sql</param> | ||||||
|  |     /// <param name="ignoreVersion">是否忽略版本锁</param> | ||||||
|  |     /// <returns>更新行数</returns> | ||||||
|  |     protected Task<int> UpdateAsync(                         // | ||||||
|  |         TEntity                         newValue             // | ||||||
|  |       , IEnumerable<string>             includeFields        // | ||||||
|  |       , string[]                        excludeFields = null // | ||||||
|  |       , Expression<Func<TEntity, bool>> whereExp      = null // | ||||||
|  |       , string                          whereSql      = null // | ||||||
|  |       , bool                            ignoreVersion = false) | ||||||
|  |     { | ||||||
|  |         // 默认匹配主键 | ||||||
|  |         whereExp ??= a => a.Id.Equals(newValue.Id); | ||||||
|  |         var update = BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql); | ||||||
|  |         return update.ExecuteAffrowsAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #if DBTYPE_SQLSERVER | ||||||
|  |     /// <summary> | ||||||
|  |     ///     更新实体 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="newValue">新的值</param> | ||||||
|  |     /// <param name="includeFields">包含的属性</param> | ||||||
|  |     /// <param name="excludeFields">排除的属性</param> | ||||||
|  |     /// <param name="whereExp">查询表达式</param> | ||||||
|  |     /// <param name="whereSql">查询sql</param> | ||||||
|  |     /// <param name="ignoreVersion">是否忽略版本锁</param> | ||||||
|  |     /// <returns>更新后的实体列表</returns> | ||||||
|  |     protected Task<List<TEntity>> UpdateReturnListAsync(     // | ||||||
|  |         TEntity                         newValue             // | ||||||
|  |       , IEnumerable<string>             includeFields        // | ||||||
|  |       , string[]                        excludeFields = null // | ||||||
|  |       , Expression<Func<TEntity, bool>> whereExp      = null // | ||||||
|  |       , string                          whereSql      = null // | ||||||
|  |       , bool                            ignoreVersion = false) | ||||||
|  |     { | ||||||
|  |         // 默认匹配主键 | ||||||
|  |         whereExp ??= a => a.Id.Equals(newValue.Id); | ||||||
|  |         return BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql).ExecuteUpdatedAsync(); | ||||||
|  |     } | ||||||
|  |     #endif | ||||||
|  |  | ||||||
|  |     private static async Task<IActionResult> GetExportFileStreamAsync<TExport>(string fileName, object list) | ||||||
|  |     { | ||||||
|  |         var listTyped = list.Adapt<List<TExport>>(); | ||||||
|  |         var stream    = new MemoryStream(); | ||||||
|  |         var writer    = new StreamWriter(stream); | ||||||
|  |         var csv       = new CsvWriter(writer, CultureInfo.InvariantCulture); | ||||||
|  |         csv.WriteHeader<TExport>(); | ||||||
|  |         await csv.NextRecordAsync().ConfigureAwait(false); | ||||||
|  |  | ||||||
|  |         foreach (var item in listTyped) { | ||||||
|  |             csv.WriteRecord(item); | ||||||
|  |             await csv.NextRecordAsync().ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await csv.FlushAsync().ConfigureAwait(false); | ||||||
|  |         _ = stream.Seek(0, SeekOrigin.Begin); | ||||||
|  |  | ||||||
|  |         App.HttpContext.Response.Headers.ContentDisposition | ||||||
|  |             = new ContentDispositionHeaderValue(Chars.FLG_HTTP_HEADER_VALUE_ATTACHMENT) { | ||||||
|  |                                                                                             FileNameStar | ||||||
|  |                                                                                                 = $"{fileName}_{DateTime.Now:yyyy.MM.dd-HH.mm.ss}.csv" | ||||||
|  |                                                                                         }.ToString(); | ||||||
|  |  | ||||||
|  |         return new FileStreamResult(stream, Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_OCTET_STREAM); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private IUpdate<TEntity> BuildUpdate(        // | ||||||
|  |         TEntity             entity               // | ||||||
|  |       , IEnumerable<string> includeFields        // | ||||||
|  |       , string[]            excludeFields = null // | ||||||
|  |       , bool                ignoreVersion = false) | ||||||
|  |     { | ||||||
|  |         var updateExp = includeFields == null | ||||||
|  |             ? Rpo.UpdateDiy.SetSource(entity) | ||||||
|  |             : Rpo.UpdateDiy.SetDto(includeFields!.ToDictionary( | ||||||
|  |                                        x => x, x => typeof(TEntity).GetProperty(x, BindingFlags.Public | BindingFlags.Instance)!.GetValue(entity))); | ||||||
|  |         if (excludeFields != null) { | ||||||
|  |             updateExp = updateExp.IgnoreColumns(excludeFields); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!ignoreVersion && entity is IFieldVersion ver) { | ||||||
|  |             updateExp = updateExp.Where($"{nameof(IFieldVersion.Version)} = @version", new { version = ver.Version }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return updateExp; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | using NetAdmin.Domain.Contexts; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract class ServiceBase<TLogger> : ServiceBase | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="ServiceBase{TLogger}" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     protected ServiceBase() // | ||||||
|  |     { | ||||||
|  |         Logger = App.GetService<ILogger<TLogger>>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     日志记录器 | ||||||
|  |     /// </summary> | ||||||
|  |     protected ILogger<TLogger> Logger { get; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     服务基类 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class ServiceBase : IScoped, IService | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="ServiceBase" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     protected ServiceBase() | ||||||
|  |     { | ||||||
|  |         UserToken = App.GetService<ContextUserToken>(); | ||||||
|  |         ServiceId = Guid.NewGuid(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Guid ServiceId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public ContextUserToken UserToken { get; set; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | using NetAdmin.Application.Modules.Tpl; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services.Tpl.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     示例服务 | ||||||
|  | /// </summary> | ||||||
|  | public interface IExampleService : IService, IExampleModule; | ||||||
| @@ -0,0 +1,128 @@ | |||||||
|  | using NetAdmin.Application.Repositories; | ||||||
|  | using NetAdmin.Application.Services.Tpl.Dependency; | ||||||
|  | using NetAdmin.Domain.DbMaps.Tpl; | ||||||
|  | using NetAdmin.Domain.Dto.Dependency; | ||||||
|  | using NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services.Tpl; | ||||||
|  |  | ||||||
|  | /// <inheritdoc cref="IExampleService" /> | ||||||
|  | public sealed class ExampleService(BasicRepository<Tpl_Example, long> rpo) // | ||||||
|  |     : RepositoryService<Tpl_Example, long, IExampleService>(rpo), IExampleService | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         var ret = 0; | ||||||
|  |  | ||||||
|  |         // ReSharper disable once LoopCanBeConvertedToQuery | ||||||
|  |         foreach (var item in req.Items) { | ||||||
|  |             ret += await DeleteAsync(item).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return ret; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<long> CountAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         return QueryInternal(req) | ||||||
|  |                #if DBTYPE_SQLSERVER | ||||||
|  |                .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) | ||||||
|  |                #endif | ||||||
|  |                .CountAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public async Task<QueryExampleRsp> CreateAsync(CreateExampleReq req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         var ret = await Rpo.InsertAsync(req).ConfigureAwait(false); | ||||||
|  |         return ret.Adapt<QueryExampleRsp>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<int> DeleteAsync(DelReq req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         return Rpo.DeleteAsync(a => a.Id == req.Id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<bool> ExistAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         return QueryInternal(req) | ||||||
|  |                #if DBTYPE_SQLSERVER | ||||||
|  |                .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) | ||||||
|  |                #endif | ||||||
|  |                .AnyAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<IActionResult> ExportAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         return ExportAsync<QueryExampleReq, QueryExampleRsp>(QueryInternal, req, Ln.示例导出); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public async Task<QueryExampleRsp> GetAsync(QueryExampleReq req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         var ret = await QueryInternal(new QueryReq<QueryExampleReq> { Filter = req, Order = Orders.None }).ToOneAsync().ConfigureAwait(false); | ||||||
|  |         return ret.Adapt<QueryExampleRsp>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public async Task<PagedQueryRsp<QueryExampleRsp>> PagedQueryAsync(PagedQueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         var list = await QueryInternal(req) | ||||||
|  |                          .Page(req.Page, req.PageSize) | ||||||
|  |                          #if DBTYPE_SQLSERVER | ||||||
|  |                          .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) | ||||||
|  |                          #endif | ||||||
|  |                          .Count(out var total) | ||||||
|  |                          .ToListAsync() | ||||||
|  |                          .ConfigureAwait(false); | ||||||
|  |  | ||||||
|  |         return new PagedQueryRsp<QueryExampleRsp>(req.Page, req.PageSize, total, list.Adapt<IEnumerable<QueryExampleRsp>>()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public async Task<IEnumerable<QueryExampleRsp>> QueryAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         req.ThrowIfInvalid(); | ||||||
|  |         var ret = await QueryInternal(req) | ||||||
|  |                         #if DBTYPE_SQLSERVER | ||||||
|  |                         .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) | ||||||
|  |                         #endif | ||||||
|  |                         .Take(req.Count) | ||||||
|  |                         .ToListAsync() | ||||||
|  |                         .ConfigureAwait(false); | ||||||
|  |         return ret.Adapt<IEnumerable<QueryExampleRsp>>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private ISelect<Tpl_Example> QueryInternal(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter).WhereDynamic(req.Filter); | ||||||
|  |  | ||||||
|  |         // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault | ||||||
|  |         switch (req.Order) { | ||||||
|  |             case Orders.None: | ||||||
|  |                 return ret; | ||||||
|  |             case Orders.Random: | ||||||
|  |                 return ret.OrderByRandom(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ret = ret.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending); | ||||||
|  |         if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) { | ||||||
|  |             ret = ret.OrderByDescending(a => a.Id); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return ret; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								src/backend/NetAdmin/NetAdmin.Cache/CacheBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/backend/NetAdmin/NetAdmin.Cache/CacheBase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | using NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Cache; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     缓存基类 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class CacheBase<TCacheContainer, TService>(TCacheContainer cache, TService service) : ICache<TCacheContainer, TService> | ||||||
|  |     where TService : IService | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public TCacheContainer Cache => cache; | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public TService Service => service; | ||||||
|  | } | ||||||
							
								
								
									
										93
									
								
								src/backend/NetAdmin/NetAdmin.Cache/DistributedCache.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/backend/NetAdmin/NetAdmin.Cache/DistributedCache.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | using System.Runtime.CompilerServices; | ||||||
|  | using NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Cache; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     分布式缓存 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class DistributedCache<TService>(IDistributedCache cache, TService service) : CacheBase<IDistributedCache, TService>(cache, service) | ||||||
|  |     where TService : IService | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="key">缓存键</param> | ||||||
|  |     /// <param name="createObj">创建对象</param> | ||||||
|  |     /// <param name="absLifeTime">绝对过期时间</param> | ||||||
|  |     /// <param name="slideLifeTime">滑动过期时间</param> | ||||||
|  |     /// <typeparam name="T">缓存对象类型</typeparam> | ||||||
|  |     /// <returns>缓存对象</returns> | ||||||
|  |     protected Task CreateAsync<T>(string key, T createObj, TimeSpan? absLifeTime = null, TimeSpan? slideLifeTime = null) | ||||||
|  |     { | ||||||
|  |         var cacheWrite = createObj.ToJson(); | ||||||
|  |  | ||||||
|  |         var options = new DistributedCacheEntryOptions(); | ||||||
|  |         if (absLifeTime != null) { | ||||||
|  |             _ = options.SetAbsoluteExpiration(absLifeTime.Value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (slideLifeTime != null) { | ||||||
|  |             _ = options.SetSlidingExpiration(slideLifeTime.Value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return Cache.SetAsync(key, cacheWrite.Hex(), options); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     protected async Task<T> GetAsync<T>(string key) | ||||||
|  |     { | ||||||
|  |         var cacheRead = await Cache.GetStringAsync(key).ConfigureAwait(false); | ||||||
|  |         try { | ||||||
|  |             return cacheRead != null ? cacheRead.ToObject<T>() : default; | ||||||
|  |         } | ||||||
|  |         catch (JsonException) { | ||||||
|  |             return default; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取缓存键 | ||||||
|  |     /// </summary> | ||||||
|  |     protected string GetCacheKey(string id = "0", [CallerMemberName] string memberName = null) | ||||||
|  |     { | ||||||
|  |         return $"{GetType().FullName}.{memberName}.{id}"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取或创建缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="key">缓存键</param> | ||||||
|  |     /// <param name="createProc">创建函数</param> | ||||||
|  |     /// <param name="absLifeTime">绝对过期时间</param> | ||||||
|  |     /// <param name="slideLifeTime">滑动过期时间</param> | ||||||
|  |     /// <typeparam name="T">缓存对象类型</typeparam> | ||||||
|  |     /// <returns>缓存对象</returns> | ||||||
|  |     protected async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> createProc, TimeSpan? absLifeTime = null, TimeSpan? slideLifeTime = null) | ||||||
|  |     { | ||||||
|  |         var cacheRead = await GetAsync<T>(key).ConfigureAwait(false); | ||||||
|  |         if (cacheRead is not null && App.HttpContext?.Request.Headers.CacheControl.FirstOrDefault() != Chars.FLG_HTTP_HEADER_VALUE_NO_CACHE) { | ||||||
|  |             return cacheRead; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var obj = await createProc.Invoke().ConfigureAwait(false); | ||||||
|  |  | ||||||
|  |         var cacheWrite = obj?.ToJson(); | ||||||
|  |         if (cacheWrite == null) { | ||||||
|  |             return obj; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await CreateAsync(key, obj, absLifeTime, slideLifeTime).ConfigureAwait(false); | ||||||
|  |         return obj; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     删除缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     protected Task RemoveAsync(string key) | ||||||
|  |     { | ||||||
|  |         return Cache.RemoveAsync(key); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								src/backend/NetAdmin/NetAdmin.Cache/ICache.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/backend/NetAdmin/NetAdmin.Cache/ICache.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | using NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Cache; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     缓存接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface ICache<out TCacheLoad, out TService> | ||||||
|  |     where TService : IService | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     缓存对象 | ||||||
|  |     /// </summary> | ||||||
|  |     TCacheLoad Cache { get; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     关联的服务 | ||||||
|  |     /// </summary> | ||||||
|  |     public TService Service { get; } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/backend/NetAdmin/NetAdmin.Cache/MemoryCache.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/backend/NetAdmin/NetAdmin.Cache/MemoryCache.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | using NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Cache; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     内存缓存 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class MemoryCache<TService>(IMemoryCache cache, TService service) : CacheBase<IMemoryCache, TService>(cache, service) | ||||||
|  |     where TService : IService; | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |     <Import Project="$(SolutionDir)/build/code.quality.props"/> | ||||||
|  |     <ItemGroup> | ||||||
|  |         <ProjectReference Include="../NetAdmin.Application/NetAdmin.Application.csproj"/> | ||||||
|  |     </ItemGroup> | ||||||
|  | </Project> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | using NetAdmin.Application.Modules.Tpl; | ||||||
|  | using NetAdmin.Application.Services.Tpl.Dependency; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Cache.Tpl.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     示例缓存 | ||||||
|  | /// </summary> | ||||||
|  | public interface IExampleCache : ICache<IDistributedCache, IExampleService>, IExampleModule; | ||||||
							
								
								
									
										65
									
								
								src/backend/NetAdmin/NetAdmin.Cache/Tpl/ExampleCache.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/backend/NetAdmin/NetAdmin.Cache/Tpl/ExampleCache.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | using NetAdmin.Application.Services.Tpl.Dependency; | ||||||
|  | using NetAdmin.Cache.Tpl.Dependency; | ||||||
|  | using NetAdmin.Domain.Dto.Dependency; | ||||||
|  | using NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Cache.Tpl; | ||||||
|  |  | ||||||
|  | /// <inheritdoc cref="IExampleCache" /> | ||||||
|  | public sealed class ExampleCache(IDistributedCache cache, IExampleService service) | ||||||
|  |     : DistributedCache<IExampleService>(cache, service), IScoped, IExampleCache | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<int> BulkDeleteAsync(BulkReq<DelReq> req) | ||||||
|  |     { | ||||||
|  |         return Service.BulkDeleteAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<long> CountAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Service.CountAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<QueryExampleRsp> CreateAsync(CreateExampleReq req) | ||||||
|  |     { | ||||||
|  |         return Service.CreateAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<int> DeleteAsync(DelReq req) | ||||||
|  |     { | ||||||
|  |         return Service.DeleteAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<bool> ExistAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Service.ExistAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<IActionResult> ExportAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Service.ExportAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<QueryExampleRsp> GetAsync(QueryExampleReq req) | ||||||
|  |     { | ||||||
|  |         return Service.GetAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<PagedQueryRsp<QueryExampleRsp>> PagedQueryAsync(PagedQueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Service.PagedQueryAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public Task<IEnumerable<QueryExampleRsp>> QueryAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Service.QueryAsync(req); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     危险字段标记 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Property)] | ||||||
|  | public sealed class DangerFieldAttribute : Attribute; | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     支付宝验证器(手机或邮箱) | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class AlipayAttribute : ValidationAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="AlipayAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public AlipayAttribute() | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.支付宝账号); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override bool IsValid(object value) | ||||||
|  |     { | ||||||
|  |         return new MobileAttribute().IsValid(value) || new EmailAddressAttribute().IsValid(value); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     证件号码验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class CertificateAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="CertificateAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public CertificateAttribute() // | ||||||
|  |         : base(Chars.RGX_CERTIFICATE) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.无效证件号码); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     中文姓名验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class ChineseNameAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="ChineseNameAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public ChineseNameAttribute() // | ||||||
|  |         : base(Chars.RGXL_CHINESE_NAME) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.中文姓名); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     时间表达式验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class CronAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="CronAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public CronAttribute() // | ||||||
|  |         : base(Chars.RGXL_CRON) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.时间表达式); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     邮箱验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class EmailAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="EmailAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public EmailAttribute() // | ||||||
|  |         : base(Chars.RGXL_EMAIL) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.电子邮箱); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     邀请码验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class InviteCodeAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="InviteCodeAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public InviteCodeAttribute() // | ||||||
|  |         : base(Chars.RGX_INVITE_CODE) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.邀请码不正确); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     JSON文本验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class JsonStringAttribute : ValidationAttribute | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     protected override ValidationResult IsValid(object value, ValidationContext validationContext) | ||||||
|  |     { | ||||||
|  |         return (value as string).IsJsonString() ? ValidationResult.Success : new ValidationResult(Ln.非JSON字符串, [validationContext.MemberName]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     手机号码验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class MobileAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="MobileAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public MobileAttribute() // | ||||||
|  |         : base(Chars.RGX_MOBILE) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.手机号码不正确); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     密码验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class PasswordAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="PasswordAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public PasswordAttribute() // | ||||||
|  |         : base(Chars.RGX_PASSWORD) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln._8位以上数字字母组合); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     交易密码验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class PayPasswordAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="PayPasswordAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public PayPasswordAttribute() // | ||||||
|  |         : base(Chars.RGX_PAY_PASSWORD) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln._6位数字); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     端口号验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class PortAttribute : RangeAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="PortAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public PortAttribute() // | ||||||
|  |         : base(1, ushort.MaxValue) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.无效端口号); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     正则表达式验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | #pragma warning disable DesignedForInheritance | ||||||
|  | public class RegexAttribute : RegularExpressionAttribute | ||||||
|  | #pragma warning restore DesignedForInheritance | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="RegexAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     protected RegexAttribute(string pattern) // | ||||||
|  |         : base(pattern) { } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     固定电话验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class TelephoneAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="TelephoneAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public TelephoneAttribute() // | ||||||
|  |         : base(Chars.RGX_TELEPHONE) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.区号电话号码分机号); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     用户名验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class UserNameAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="UserNameAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public UserNameAttribute() // | ||||||
|  |         : base(Chars.RGX_USERNAME) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override bool IsValid(object value) | ||||||
|  |     { | ||||||
|  |         if (!base.IsValid(value)) { | ||||||
|  |             ErrorMessageResourceName = nameof(Ln.用户名长度4位以上); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!new MobileAttribute().IsValid(value)) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 不能是手机号码 | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.用户名不能是手机号码); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes.DataValidation; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     验证码验证器 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] | ||||||
|  | public sealed class VerifyCodeAttribute : RegexAttribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="VerifyCodeAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public VerifyCodeAttribute() // | ||||||
|  |         : base(Chars.RGX_VERIFY_CODE) | ||||||
|  |     { | ||||||
|  |         ErrorMessageResourceName = nameof(Ln.验证码不正确); | ||||||
|  |         ErrorMessageResourceType = typeof(Ln); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     标记一个枚举的状态指示 | ||||||
|  | /// </summary> | ||||||
|  | /// <inheritdoc /> | ||||||
|  | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Enum)] | ||||||
|  | public sealed class IndicatorAttribute(string indicate) : Attribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     状态指示 | ||||||
|  |     /// </summary> | ||||||
|  |     public string Indicate { get; } = indicate; | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     标记一个字段启用服务器时间 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Property)] | ||||||
|  | public sealed class ServerTimeAttribute : Attribute; | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Domain.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     标记一个字段启用雪花编号生成 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Property)] | ||||||
|  | public sealed class SnowflakeAttribute : Attribute; | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | namespace NetAdmin.Domain.Contexts; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     上下文用户凭据 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record ContextUserToken : DataAbstraction | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     部门编号 | ||||||
|  |     /// </summary> | ||||||
|  |     /// ReSharper disable once MemberCanBePrivate.Global | ||||||
|  |     public long DeptId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     用户编号 | ||||||
|  |     /// </summary> | ||||||
|  |     /// ReSharper disable once MemberCanBePrivate.Global | ||||||
|  |     public long Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     做授权验证的Token,全局唯一,可以随时重置(强制下线) | ||||||
|  |     /// </summary> | ||||||
|  |     /// ReSharper disable once MemberCanBePrivate.Global | ||||||
|  |     public Guid Token { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     /// ReSharper disable once MemberCanBePrivate.Global | ||||||
|  |     public string UserName { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     从HttpContext 创建上下文用户 | ||||||
|  |     /// </summary> | ||||||
|  |     public static ContextUserToken Create() | ||||||
|  |     { | ||||||
|  |         var claim = App.User?.FindFirst(nameof(ContextUserToken)); | ||||||
|  |         return claim?.Value.ToObject<ContextUserToken>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     从 QueryUserRsp 创建上下文用户 | ||||||
|  |     /// </summary> | ||||||
|  |     public static ContextUserToken Create(long id, Guid token, string userName, long deptId) | ||||||
|  |     { | ||||||
|  |         return new ContextUserToken { Id = id, Token = token, UserName = userName, DeptId = deptId }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     从 Json Web Token 创建上下文用户 | ||||||
|  |     /// </summary> | ||||||
|  |     public static ContextUserToken Create(string jwt) | ||||||
|  |     { | ||||||
|  |         var claim = JWTEncryption.ReadJwtToken(jwt.TrimPrefix($"{Chars.FLG_HTTP_HEADER_VALUE_AUTH_SCHEMA} ")) | ||||||
|  |                                  ?.Claims.FirstOrDefault(x => x.Type == nameof(ContextUserToken)); | ||||||
|  |         return claim?.Value.ToObject<ContextUserToken>(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								src/backend/NetAdmin/NetAdmin.Domain/DataAbstraction.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/backend/NetAdmin/NetAdmin.Domain/DataAbstraction.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | namespace NetAdmin.Domain; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     数据基类 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record DataAbstraction | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     如果数据校验失败,抛出异常 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <exception cref="NetAdminValidateException">NetAdminValidateException</exception> | ||||||
|  |     public void ThrowIfInvalid() | ||||||
|  |     { | ||||||
|  |         var validationResult = this.TryValidate(); | ||||||
|  |         if (!validationResult.IsValid) { | ||||||
|  |             throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( // | ||||||
|  |                                                     x => x.MemberNames.First()                   // | ||||||
|  |                                                   , x => new[] { x.ErrorMessage })); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override string ToString() | ||||||
|  |     { | ||||||
|  |         return this.ToJson(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     截断所有字符串属性 以符合[MaxLength(x)]特性 | ||||||
|  |     /// </summary> | ||||||
|  |     public void TruncateStrings() | ||||||
|  |     { | ||||||
|  |         foreach (var property in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.PropertyType == typeof(string))) { | ||||||
|  |             var maxLen = property.GetCustomAttribute<MaxLengthAttribute>(true)?.Length; | ||||||
|  |             if (maxLen is null or 0) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var value = property.GetValue(this); | ||||||
|  |             if (value is not string s || s.Length < maxLen) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             s = s.Sub(0, maxLen.Value); | ||||||
|  |             property.SetValue(this, s); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     数据库实体基类 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record EntityBase<T> : DataAbstraction | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     public virtual T Id { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     创建者客户端IP字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldCreatedClientIp | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者客户端IP | ||||||
|  |     /// </summary> | ||||||
|  |     int? CreatedClientIp { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     创建者客户端用户代理字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldCreatedClientUserAgent | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者客户端用户代理 | ||||||
|  |     /// </summary> | ||||||
|  |     string CreatedUserAgent { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     创建时间字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldCreatedTime | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建时间 | ||||||
|  |     /// </summary> | ||||||
|  |     DateTime CreatedTime { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     创建用户字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldCreatedUser | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     long? CreatedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     string CreatedUserName { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     启用字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldEnabled | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     是否启用 | ||||||
|  |     /// </summary> | ||||||
|  |     bool Enabled { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     修改客户端IP字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldModifiedClientIp | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     客户端IP | ||||||
|  |     /// </summary> | ||||||
|  |     int ModifiedClientIp { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     修改客户端用户代理字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldModifiedClientUserAgent | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     客户端用户代理 | ||||||
|  |     /// </summary> | ||||||
|  |     string ModifiedUserAgent { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     修改时间字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldModifiedTime | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改时间 | ||||||
|  |     /// </summary> | ||||||
|  |     DateTime? ModifiedTime { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     修改用户字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldModifiedUser | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     long? ModifiedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     string ModifiedUserName { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     拥有者字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldOwner | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     拥有者部门编号 | ||||||
|  |     /// </summary> | ||||||
|  |     long? OwnerDeptId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     拥有者用户编号 | ||||||
|  |     /// </summary> | ||||||
|  |     long? OwnerId { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     排序字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldSort | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     排序值,越大越前 | ||||||
|  |     /// </summary> | ||||||
|  |     long Sort { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     备注字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldSummary | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     备注 | ||||||
|  |     /// </summary> | ||||||
|  |     string Summary { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     版本字段接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IFieldVersion | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     数据版本 | ||||||
|  |     /// </summary> | ||||||
|  |     long Version { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,44 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record ImmutableEntity : ImmutableEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     不可变实体 | ||||||
|  | /// </summary> | ||||||
|  | /// <typeparam name="T">主键类型</typeparam> | ||||||
|  | public abstract record ImmutableEntity<T> : LiteImmutableEntity<T>, IFieldCreatedUser | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public long? CreatedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual string CreatedUserName { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     public override T Id { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record LiteImmutableEntity : LiteImmutableEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     轻型不可变实体 | ||||||
|  | /// </summary> | ||||||
|  | /// <typeparam name="T">主键类型</typeparam> | ||||||
|  | public abstract record LiteImmutableEntity<T> : EntityBase<T>, IFieldCreatedTime | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建时间 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(ServerTime = DateTimeKind.Local, CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual DateTime CreatedTime { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public override T Id { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record LiteMutableEntity : LiteMutableEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     轻型可变实体 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record LiteMutableEntity<T> : LiteImmutableEntity<T>, IFieldModifiedTime | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     public override T Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改时间 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(ServerTime = DateTimeKind.Local, CanInsert = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual DateTime? ModifiedTime { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record LiteVersionEntity : LiteVersionEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     乐观锁轻型可变实体 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record LiteVersionEntity<T> : LiteMutableEntity<T>, IFieldVersion | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override T Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     数据版本 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsVersion = true, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual long Version { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record MutableEntity : MutableEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     可变实体 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record MutableEntity<T> : LiteMutableEntity<T>, IFieldCreatedUser, IFieldModifiedUser | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual long? CreatedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual string CreatedUserName { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     public override T Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(CanInsert = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public long? ModifiedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public string ModifiedUserName { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record SimpleEntity : SimpleEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     简单实体 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record SimpleEntity<T> : EntityBase<T> | ||||||
|  |     where T : IEquatable<T>; | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc /> | ||||||
|  | public abstract record VersionEntity : VersionEntity<long> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [Snowflake] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     乐观锁可变实体 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record VersionEntity<T> : LiteVersionEntity<T>, IFieldModifiedUser, IFieldCreatedUser | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual long? CreatedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual string CreatedUserName { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     唯一编码 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(IsIdentity = false, IsPrimary = true, Position = 1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     public override T Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改者编号 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(CanInsert = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual long? ModifiedUserId { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     修改者用户名 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual string ModifiedUserName { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | namespace NetAdmin.Domain.DbMaps.Tpl; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     示例表 | ||||||
|  | /// </summary> | ||||||
|  | [Table(Name = Chars.FLG_DB_TABLE_NAME_PREFIX + nameof(Tpl_Example))] | ||||||
|  | public record Tpl_Example : VersionEntity; | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     批量请求 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record BulkReq<T> : DataAbstraction | ||||||
|  |     where T : DataAbstraction, new() | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     请求对象 | ||||||
|  |     /// </summary> | ||||||
|  |     [MaxLength(Numbers.MAX_LIMIT_BULK_REQ)] | ||||||
|  |     [MinLength(1)] | ||||||
|  |     [Required(ErrorMessageResourceType = typeof(Ln), ErrorMessageResourceName = nameof(Ln.请求对象不能为空))] | ||||||
|  |     public IEnumerable<T> Items { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <inheritdoc cref="DelReq{T}" /> | ||||||
|  | public sealed record DelReq : DelReq<long>; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     请求:通过编号删除 | ||||||
|  | /// </summary> | ||||||
|  | public record DelReq<T> : EntityBase<T> | ||||||
|  |     where T : IEquatable<T> | ||||||
|  | { | ||||||
|  |     /// <inheritdoc cref="EntityBase{T}.Id" /> | ||||||
|  |     public override T Id { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     信息:分页 | ||||||
|  | /// </summary> | ||||||
|  | public interface IPagedInfo | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     当前页码 | ||||||
|  |     /// </summary> | ||||||
|  |     int Page { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     页容量 | ||||||
|  |     /// </summary> | ||||||
|  |     int PageSize { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     空请求 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record NopReq : DataAbstraction; | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     请求:分页查询 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record PagedQueryReq<T> : QueryReq<T>, IPagedInfo | ||||||
|  |     where T : DataAbstraction, new() | ||||||
|  | { | ||||||
|  |     /// <inheritdoc cref="IPagedInfo.Page" /> | ||||||
|  |     [Range(1, Numbers.MAX_LIMIT_QUERY_PAGE_NO)] | ||||||
|  |     public int Page { get; init; } = 1; | ||||||
|  |  | ||||||
|  |     /// <inheritdoc cref="IPagedInfo.PageSize" /> | ||||||
|  |     [Range(1, Numbers.MAX_LIMIT_QUERY_PAGE_SIZE)] | ||||||
|  |     public int PageSize { get; init; } = Numbers.DEF_PAGE_SIZE_QUERY; | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     响应:分页查询 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record PagedQueryRsp<T>(int Page, int PageSize, long Total, IEnumerable<T> Rows) : IPagedInfo | ||||||
|  |     where T : DataAbstraction | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     数据行 | ||||||
|  |     /// </summary> | ||||||
|  |     public IEnumerable<T> Rows { get; } = Rows; | ||||||
|  |  | ||||||
|  |     /// <inheritdoc cref="IPagedInfo.Page" /> | ||||||
|  |     public int Page { get; init; } = Page; | ||||||
|  |  | ||||||
|  |     /// <inheritdoc cref="IPagedInfo.PageSize" /> | ||||||
|  |     public int PageSize { get; init; } = PageSize; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     数据总条 | ||||||
|  |     /// </summary> | ||||||
|  |     public long Total { get; init; } = Total; | ||||||
|  | } | ||||||
| @@ -0,0 +1,72 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto.Dependency; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     请求:查询 | ||||||
|  | /// </summary> | ||||||
|  | public record QueryReq<T> : DataAbstraction | ||||||
|  |     where T : DataAbstraction, new() | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     取前n条 | ||||||
|  |     /// </summary> | ||||||
|  |     [Range(1, Numbers.MAX_LIMIT_QUERY)] | ||||||
|  |     public int Count { get; init; } = Numbers.MAX_LIMIT_QUERY; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     动态查询条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterInfo DynamicFilter { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     查询条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public T Filter { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     查询关键字 | ||||||
|  |     /// </summary> | ||||||
|  |     public string Keywords { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     排序方式 | ||||||
|  |     /// </summary> | ||||||
|  |     public Orders? Order { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     排序字段 | ||||||
|  |     /// </summary> | ||||||
|  |     public string Prop { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     所需字段 | ||||||
|  |     /// </summary> | ||||||
|  |     public string[] RequiredFields { get; set; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     列表表达式 | ||||||
|  |     /// </summary> | ||||||
|  |     public Expression<Func<TEntity, TEntity>> GetToListExp<TEntity>() | ||||||
|  |     { | ||||||
|  |         if (RequiredFields.NullOrEmpty()) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var expParameter = Expression.Parameter(typeof(TEntity), "a"); | ||||||
|  |         var bindings     = new List<MemberBinding>(); | ||||||
|  |  | ||||||
|  |         // ReSharper disable once LoopCanBeConvertedToQuery | ||||||
|  |         foreach (var field in RequiredFields) { | ||||||
|  |             var prop = typeof(TEntity).GetProperty(field); | ||||||
|  |             if (prop == null || prop.GetCustomAttribute<DangerFieldAttribute>() != null) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var propExp = Expression.Property(expParameter, prop); | ||||||
|  |             var binding = Expression.Bind(prop, propExp); | ||||||
|  |             bindings.Add(binding); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var expBody = Expression.MemberInit(Expression.New(typeof(TEntity)), bindings); | ||||||
|  |         return Expression.Lambda<Func<TEntity, TEntity>>(expBody, expParameter); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Dto/DfBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Dto/DfBuilder.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | using NetAdmin.Domain.Enums; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Domain.Dto; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     动态过滤条件生成器 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record DfBuilder | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     构建生成器 | ||||||
|  |     /// </summary> | ||||||
|  |     public static DynamicFilterInfo New(DynamicFilterLogics logic) | ||||||
|  |     { | ||||||
|  |         return new DynamicFilterInfo { Logic = logic, Filters = [] }; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										125
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Dto/DynamicFilterInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Dto/DynamicFilterInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  | using NetAdmin.Domain.Enums; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Domain.Dto; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     动态过滤条件 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record DynamicFilterInfo : DataAbstraction | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     字段名 | ||||||
|  |     /// </summary> | ||||||
|  |     public string Field { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     子过滤条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public List<DynamicFilterInfo> Filters { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     子过滤条件逻辑关系 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterLogics Logic { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     操作符 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterOperators Operator { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     值 | ||||||
|  |     /// </summary> | ||||||
|  |     public object Value { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     隐式转换为 FreeSql 的 DynamicFilterInfo 对象 | ||||||
|  |     /// </summary> | ||||||
|  |     public static implicit operator FreeSql.Internal.Model.DynamicFilterInfo(DynamicFilterInfo d) | ||||||
|  |     { | ||||||
|  |         var ret = d.Adapt<FreeSql.Internal.Model.DynamicFilterInfo>(); | ||||||
|  |         ProcessDynamicFilter(ret); | ||||||
|  |         return ret; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加子过滤条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterInfo Add(DynamicFilterInfo df) | ||||||
|  |     { | ||||||
|  |         if (Filters == null) { | ||||||
|  |             return this with { Filters = [df] }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Filters.Add(df); | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加过滤条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterInfo Add(string field, DynamicFilterOperators opt, object val) | ||||||
|  |     { | ||||||
|  |         return Add(new DynamicFilterInfo { Field = field, Operator = opt, Value = val }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加过滤条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterInfo AddIf(bool condition, string field, DynamicFilterOperators opt, object val) | ||||||
|  |     { | ||||||
|  |         return !condition ? this : Add(field, opt, val); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加过滤条件 | ||||||
|  |     /// </summary> | ||||||
|  |     public DynamicFilterInfo AddIf(bool condition, DynamicFilterInfo df) | ||||||
|  |     { | ||||||
|  |         return !condition ? this : Add(df); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static void ParseDateExp(FreeSql.Internal.Model.DynamicFilterInfo d) | ||||||
|  |     { | ||||||
|  |         var values = ((JsonElement)d.Value).Deserialize<string[]>(); | ||||||
|  |         if (!DateTime.TryParse(values[0], CultureInfo.InvariantCulture, out _)) { | ||||||
|  |             var result = values[0] | ||||||
|  |                          .ExecuteCSharpCodeAsync<DateTime>([typeof(DateTime).Assembly], nameof(System)) | ||||||
|  |                          .ConfigureAwait(false) | ||||||
|  |                          .GetAwaiter() | ||||||
|  |                          .GetResult(); | ||||||
|  |             values[0] = $"{result:yyyy-MM-dd HH:mm:ss}"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!DateTime.TryParse(values[1], CultureInfo.InvariantCulture, out _)) { | ||||||
|  |             var result = values[1] | ||||||
|  |                          .ExecuteCSharpCodeAsync<DateTime>([typeof(DateTime).Assembly], nameof(System)) | ||||||
|  |                          .ConfigureAwait(false) | ||||||
|  |                          .GetAwaiter() | ||||||
|  |                          .GetResult(); | ||||||
|  |             values[1] = $"{result:yyyy-MM-dd HH:mm:ss}"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         d.Value = values; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static void ProcessDynamicFilter(FreeSql.Internal.Model.DynamicFilterInfo d) | ||||||
|  |     { | ||||||
|  |         if (d?.Filters != null) { | ||||||
|  |             foreach (var filterInfo in d.Filters) { | ||||||
|  |                 ProcessDynamicFilter(filterInfo); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (new[] { nameof(IFieldCreatedClientIp.CreatedClientIp), nameof(IFieldModifiedClientIp.ModifiedClientIp) }.Contains( | ||||||
|  |                 d?.Field, StringComparer.OrdinalIgnoreCase)) { | ||||||
|  |             var val = d!.Value?.ToString(); | ||||||
|  |             if (val?.IsIpV4() == true) { | ||||||
|  |                 d.Value = val.IpV4ToInt32(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (d?.Operator == DynamicFilterOperator.DateRange) { | ||||||
|  |             ParseDateExp(d); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Dto/RestfulInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Dto/RestfulInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | namespace NetAdmin.Domain.Dto; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     信息:RESTful 风格结果集 | ||||||
|  | /// </summary> | ||||||
|  | public record RestfulInfo<T> : DataAbstraction | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     代码 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <example>succeed</example> | ||||||
|  |     [JsonIgnore(Condition = JsonIgnoreCondition.Never)] | ||||||
|  |     public ErrorCodes Code { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     数据 | ||||||
|  |     /// </summary> | ||||||
|  |     public T Data { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     字符串:"消息内容",或数组:[{"参数名1":"消息内容1"},{"参数名2":"消息内容2"}] | ||||||
|  |     /// </summary> | ||||||
|  |     /// <example>请求成功</example> | ||||||
|  |     public object Msg { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | using NetAdmin.Domain.DbMaps.Tpl; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     请求:创建示例 | ||||||
|  | /// </summary> | ||||||
|  | public record CreateExampleReq : Tpl_Example; | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | using NetAdmin.Domain.DbMaps.Tpl; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     请求:查询示例 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record QueryExampleReq : Tpl_Example | ||||||
|  | { | ||||||
|  |     /// <inheritdoc cref="EntityBase{T}.Id" /> | ||||||
|  |     [JsonIgnore(Condition = JsonIgnoreCondition.Never)] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | using NetAdmin.Domain.DbMaps.Tpl; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     响应:查询示例 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record QueryExampleRsp : Tpl_Example | ||||||
|  | { | ||||||
|  |     /// <inheritdoc cref="EntityBase{T}.Id" /> | ||||||
|  |     [JsonIgnore(Condition = JsonIgnoreCondition.Never)] | ||||||
|  |     public override long Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc cref="IFieldVersion.Version" /> | ||||||
|  |     [JsonIgnore(Condition = JsonIgnoreCondition.Never)] | ||||||
|  |     public override long Version { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | namespace NetAdmin.Domain.Enums; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     动态查询条件逻辑运算符 | ||||||
|  | /// </summary> | ||||||
|  | [Export] | ||||||
|  | public enum DynamicFilterLogics | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     并且 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.并且))] | ||||||
|  |     And = 0 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     或者 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.或者))] | ||||||
|  |     Or = 1 | ||||||
|  | } | ||||||
| @@ -0,0 +1,158 @@ | |||||||
|  | namespace NetAdmin.Domain.Enums; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     动态查询条件操作符 | ||||||
|  | /// </summary> | ||||||
|  | [Export] | ||||||
|  | public enum DynamicFilterOperators | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     包含 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.包含))] | ||||||
|  |     Contains = 0 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     以什么开始 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.以什么开始))] | ||||||
|  |     StartsWith = 1 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     以什么结束 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.以什么结束))] | ||||||
|  |     EndsWith = 2 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     不包含 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.不包含))] | ||||||
|  |     NotContains = 3 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     不以什么开始 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.不以什么开始))] | ||||||
|  |     NotStartsWith = 4 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     不以什么结束 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.不以什么结束))] | ||||||
|  |     NotEndsWith = 5 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     等于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.等于))] | ||||||
|  |     Equal = 6 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     等于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.等于))] | ||||||
|  |     Equals = 7 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     等于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.等于))] | ||||||
|  |     Eq = 8 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     不等于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.不等于))] | ||||||
|  |     NotEqual = 9 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     大于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.大于))] | ||||||
|  |     GreaterThan = 10 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     大于等于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.大于等于))] | ||||||
|  |     GreaterThanOrEqual = 11 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     小于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.小于))] | ||||||
|  |     LessThan = 12 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     小于等于 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.小于等于))] | ||||||
|  |     LessThanOrEqual = 13 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     范围 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.范围))] | ||||||
|  |     Range = 14 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     日期范围 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.日期范围))] | ||||||
|  |     DateRange = 15 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     为其中之一 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.为其中之一))] | ||||||
|  |     Any = 16 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     不为其中之一 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.不为其中之一))] | ||||||
|  |     NotAny = 17 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     自定义 | ||||||
|  |     /// </summary> | ||||||
|  |     [ResourceDescription<Ln>(nameof(Ln.自定义))] | ||||||
|  |     Custom = 18 | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Enums/HttpMethods.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/backend/NetAdmin/NetAdmin.Domain/Enums/HttpMethods.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | namespace NetAdmin.Domain.Enums; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     HTTP 请求方法 | ||||||
|  | /// </summary> | ||||||
|  | [Export] | ||||||
|  | public enum HttpMethods | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Connect | ||||||
|  |     /// </summary> | ||||||
|  |     Connect = 1 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Delete | ||||||
|  |     /// </summary> | ||||||
|  |     Delete = 2 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Get | ||||||
|  |     /// </summary> | ||||||
|  |     Get = 3 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Head | ||||||
|  |     /// </summary> | ||||||
|  |     Head = 4 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Options | ||||||
|  |     /// </summary> | ||||||
|  |     Options = 5 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Patch | ||||||
|  |     /// </summary> | ||||||
|  |     Patch = 6 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Post | ||||||
|  |     /// </summary> | ||||||
|  |     Post = 7 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Put | ||||||
|  |     /// </summary> | ||||||
|  |     Put = 8 | ||||||
|  |  | ||||||
|  |    , | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Trace | ||||||
|  |     /// </summary> | ||||||
|  |     Trace = 9 | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     泛型事件源接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IEventSourceGeneric<out T> : IEventSource | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     事件承载(携带)数据 | ||||||
|  |     /// </summary> | ||||||
|  |     T Data { get; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     种子数据插入完毕事件 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record SeedDataInsertedEvent : DataAbstraction, IEventSource | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="SeedDataInsertedEvent" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public SeedDataInsertedEvent(int insertedCount, bool isConsumOnce = false) | ||||||
|  |     { | ||||||
|  |         IsConsumOnce  = isConsumOnce; | ||||||
|  |         InsertedCount = insertedCount; | ||||||
|  |         CreatedTime   = DateTime.Now; | ||||||
|  |         EventId       = nameof(SeedDataInsertedEvent); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public DateTime CreatedTime { get; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public string EventId { get; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public bool IsConsumOnce { get; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public CancellationToken CancellationToken { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     插入数量 | ||||||
|  |     /// </summary> | ||||||
|  |     public int InsertedCount { get; set; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public object Payload { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Sql命令执行后事件 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record SqlCommandAfterEvent : SqlCommandBeforeEvent | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="SqlCommandAfterEvent" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public SqlCommandAfterEvent(CommandAfterEventArgs e) // | ||||||
|  |         : base(e) | ||||||
|  |     { | ||||||
|  |         ElapsedMilliseconds = (long)((double)e.ElapsedTicks / Stopwatch.Frequency * 1_000); | ||||||
|  |         EventId             = nameof(SqlCommandAfterEvent); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     耗时(单位:毫秒) | ||||||
|  |     /// </summary> | ||||||
|  |     /// de | ||||||
|  |     private long ElapsedMilliseconds { get; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override string ToString() | ||||||
|  |     { | ||||||
|  |         return string.Format(CultureInfo.InvariantCulture, "SQL-{0}: {2} ms {1}", Id, Sql, ElapsedMilliseconds); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Sql命令执行前事件 | ||||||
|  | /// </summary> | ||||||
|  | public record SqlCommandBeforeEvent : SqlCommandEvent | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="SqlCommandBeforeEvent" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public SqlCommandBeforeEvent(CommandBeforeEventArgs e) | ||||||
|  |     { | ||||||
|  |         Identifier  = e.Identifier; | ||||||
|  |         Sql         = e.Command.ParameterFormat().RemoveWrapped(); | ||||||
|  |         EventId     = nameof(SqlCommandBeforeEvent); | ||||||
|  |         CreatedTime = DateTime.Now; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override string ToString() | ||||||
|  |     { | ||||||
|  |         return string.Format(CultureInfo.InvariantCulture, "SQL-{0}: Executing...", Id); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Sql命令事件 | ||||||
|  | /// </summary> | ||||||
|  | public abstract record SqlCommandEvent : DataAbstraction, IEventSource | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="SqlCommandEvent" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     protected SqlCommandEvent(bool isConsumOnce = false) | ||||||
|  |     { | ||||||
|  |         IsConsumOnce = isConsumOnce; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public bool IsConsumOnce { get; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public CancellationToken CancellationToken { get; init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public DateTime CreatedTime { get; protected init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public string EventId { get; protected init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public object Payload { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     标识符缩写 | ||||||
|  |     /// </summary> | ||||||
|  |     protected string Id => Identifier.ToString()[..8].ToUpperInvariant(); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     标识符,可将 CommandBefore 与 CommandAfter 进行匹配 | ||||||
|  |     /// </summary> | ||||||
|  |     protected Guid Identifier { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     关联的Sql语句 | ||||||
|  |     /// </summary> | ||||||
|  |     protected string Sql { get; init; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     同步数据库结构之后事件 | ||||||
|  | /// </summary> | ||||||
|  | public sealed record SyncStructureAfterEvent : SyncStructureBeforeEvent | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="SyncStructureAfterEvent" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public SyncStructureAfterEvent(SyncStructureBeforeEventArgs e) // | ||||||
|  |         : base(e) | ||||||
|  |     { | ||||||
|  |         EventId = nameof(SyncStructureAfterEvent); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override string ToString() | ||||||
|  |     { | ||||||
|  |         return string.Format(CultureInfo.InvariantCulture, "{0}: {1}: {2}", Id, Ln.数据库结构同步完成, string.Join(',', EntityTypes.Select(x => x.Name))); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | namespace NetAdmin.Domain.Events; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     同步数据库结构之前事件 | ||||||
|  | /// </summary> | ||||||
|  | public record SyncStructureBeforeEvent : SqlCommandEvent | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="SyncStructureBeforeEvent" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public SyncStructureBeforeEvent(SyncStructureBeforeEventArgs e) | ||||||
|  |     { | ||||||
|  |         Identifier  = e.Identifier; | ||||||
|  |         EventId     = nameof(SyncStructureBeforeEvent); | ||||||
|  |         CreatedTime = DateTime.Now; | ||||||
|  |         EntityTypes = e.EntityTypes; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     实体类型 | ||||||
|  |     /// </summary> | ||||||
|  |     protected Type[] EntityTypes { get; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public override string ToString() | ||||||
|  |     { | ||||||
|  |         return string.Format(CultureInfo.InvariantCulture, "{0}: {1}", Id, Ln.数据库同步开始); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/backend/NetAdmin/NetAdmin.Domain/NetAdmin.Domain.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/backend/NetAdmin/NetAdmin.Domain/NetAdmin.Domain.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |     <Import Project="$(SolutionDir)/build/code.quality.props"/> | ||||||
|  |     <ItemGroup> | ||||||
|  |         <ProjectReference Include="../NetAdmin.Infrastructure/NetAdmin.Infrastructure.csproj"/> | ||||||
|  |     </ItemGroup> | ||||||
|  |     <ItemGroup> | ||||||
|  |         <PackageReference Include="CronExpressionDescriptor" Version="2.36.0"/> | ||||||
|  |         <PackageReference Include="Cronos" Version="0.8.4"/> | ||||||
|  |         <PackageReference Include="CsvHelper.NS" Version="33.0.2-ns2"/> | ||||||
|  |         <PackageReference Include="Yitter.IdGenerator" Version="1.0.14"/> | ||||||
|  |     </ItemGroup> | ||||||
|  | </Project> | ||||||
							
								
								
									
										5
									
								
								src/backend/NetAdmin/NetAdmin.Domain/ProjectUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/backend/NetAdmin/NetAdmin.Domain/ProjectUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | global using NetAdmin.Domain.Attributes; | ||||||
|  | global using NetAdmin.Domain.DbMaps.Dependency; | ||||||
|  | global using NetAdmin.Domain.DbMaps.Dependency.Fields; | ||||||
|  | global using CsvIgnore = CsvHelper.Configuration.Attributes.IgnoreAttribute; | ||||||
|  | global using DynamicFilterOperators = NetAdmin.Domain.Enums.DynamicFilterOperators; | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | namespace NetAdmin.Host.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     标记一个Action,其响应的json结果会被删除值为null的节点 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Method)] | ||||||
|  | public sealed class RemoveNullNodeAttribute : Attribute; | ||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | namespace NetAdmin.Host.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     标记一个Action启用事务 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Method)] | ||||||
|  | public sealed class TransactionAttribute : Attribute | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="TransactionAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public TransactionAttribute() { } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="TransactionAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public TransactionAttribute(Propagation propagation) // | ||||||
|  |         : this(null, propagation) { } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="TransactionAttribute" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     public TransactionAttribute(IsolationLevel isolationLevel, Propagation propagation) // | ||||||
|  |         : this(new IsolationLevel?(isolationLevel), propagation) { } | ||||||
|  |  | ||||||
|  |     private TransactionAttribute(IsolationLevel? isolationLevel, Propagation propagation) | ||||||
|  |     { | ||||||
|  |         IsolationLevel = isolationLevel; | ||||||
|  |         Propagation    = propagation; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     事务隔离级别 | ||||||
|  |     /// </summary> | ||||||
|  |     public IsolationLevel? IsolationLevel { get; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     事务传播方式 | ||||||
|  |     /// </summary> | ||||||
|  |     public Propagation Propagation { get; } = Propagation.Required; | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace NetAdmin.Host.BackgroundRunning; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     轮询工作接口 | ||||||
|  | /// </summary> | ||||||
|  | public interface IPollingWork | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     启动工作 | ||||||
|  |     /// </summary> | ||||||
|  |     ValueTask StartAsync(CancellationToken cancelToken); | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | using NetAdmin.Domain; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.BackgroundRunning; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     轮询工作 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class PollingWork<TWorkData>(TWorkData workData) : WorkBase<TWorkData> | ||||||
|  |     where TWorkData : DataAbstraction | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     工作数据 | ||||||
|  |     /// </summary> | ||||||
|  |     protected TWorkData WorkData => workData; | ||||||
|  | } | ||||||
| @@ -0,0 +1,75 @@ | |||||||
|  | using StackExchange.Redis; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.BackgroundRunning; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     工作基类 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class WorkBase<TLogger> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="WorkBase{TLogger}" /> class. | ||||||
|  |     /// </summary> | ||||||
|  |     protected WorkBase() | ||||||
|  |     { | ||||||
|  |         ServiceProvider = App.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider; | ||||||
|  |         UowManager      = ServiceProvider.GetService<UnitOfWorkManager>(); | ||||||
|  |         Logger          = ServiceProvider.GetService<ILogger<TLogger>>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     日志记录器 | ||||||
|  |     /// </summary> | ||||||
|  |     protected ILogger<TLogger> Logger { get; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     服务提供器 | ||||||
|  |     /// </summary> | ||||||
|  |     protected IServiceProvider ServiceProvider { get; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     事务单元管理器 | ||||||
|  |     /// </summary> | ||||||
|  |     protected UnitOfWorkManager UowManager { get; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     通用工作流 | ||||||
|  |     /// </summary> | ||||||
|  |     protected abstract ValueTask WorkflowAsync( // | ||||||
|  |  | ||||||
|  |         // ReSharper disable once UnusedParameter.Global | ||||||
|  |         #pragma warning disable SA1114 | ||||||
|  |         CancellationToken cancelToken); | ||||||
|  |     #pragma warning restore SA1114 | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     通用工作流 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <exception cref="NetAdminGetLockerException">加锁失败异常</exception> | ||||||
|  |     protected async ValueTask WorkflowAsync(bool singleInstance, CancellationToken cancelToken) | ||||||
|  |     { | ||||||
|  |         if (singleInstance) { | ||||||
|  |             // 加锁 | ||||||
|  |             var             lockName    = GetType().FullName; | ||||||
|  |             await using var redisLocker = await GetLockerAsync(lockName).ConfigureAwait(false); | ||||||
|  |  | ||||||
|  |             await WorkflowAsync(cancelToken).ConfigureAwait(false); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await WorkflowAsync(cancelToken).ConfigureAwait(false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取锁 | ||||||
|  |     /// </summary> | ||||||
|  |     private Task<RedisLocker> GetLockerAsync(string lockId) | ||||||
|  |     { | ||||||
|  |         var db = ServiceProvider.GetService<IConnectionMultiplexer>() | ||||||
|  |                                 .GetDatabase(ServiceProvider.GetService<IOptions<RedisOptions>>() | ||||||
|  |                                                             .Value.Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE) | ||||||
|  |                                                             .Database); | ||||||
|  |         return RedisLocker.GetLockerAsync(db, lockId, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), Numbers.MAX_LIMIT_RETRY_CNT_REDIS_LOCK | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | using NetAdmin.Application.Services; | ||||||
|  | using NetAdmin.Cache; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Controllers; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     控制器基类 | ||||||
|  | /// </summary> | ||||||
|  | public abstract class ControllerBase<TCache, TService>(TCache cache = default) : IDynamicApiController | ||||||
|  |     where TCache : ICache<IDistributedCache, TService> // | ||||||
|  |     where TService : IService | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     关联的缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     protected TCache Cache => cache; | ||||||
|  | } | ||||||
| @@ -0,0 +1,98 @@ | |||||||
|  | using NetAdmin.Application.Services; | ||||||
|  | using NetAdmin.Cache; | ||||||
|  | using NetAdmin.Host.Middlewares; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Controllers; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     探针组件 | ||||||
|  | /// </summary> | ||||||
|  | [ApiDescriptionSettings("Probe")] | ||||||
|  | [Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)] | ||||||
|  | public sealed class ProbeController : ControllerBase<ICache<IDistributedCache, IService>, IService> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     退出程序 | ||||||
|  |     /// </summary> | ||||||
|  |     [AllowAnonymous] | ||||||
|  |     [HttpGet] | ||||||
|  |     #pragma warning disable CA1822 | ||||||
|  |     public ActionResult Exit(string token) | ||||||
|  |         #pragma warning restore CA1822 | ||||||
|  |     { | ||||||
|  |         if (token != GlobalStatic.SecretKey) { | ||||||
|  |             return new UnauthorizedResult(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Environment.Exit(0); | ||||||
|  |         return new OkResult(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     健康检查 | ||||||
|  |     /// </summary> | ||||||
|  |     [AllowAnonymous] | ||||||
|  |     [HttpGet] | ||||||
|  |     #pragma warning disable CA1822, S3400 | ||||||
|  |     public object HealthCheck() | ||||||
|  |         #pragma warning restore S3400, CA1822 | ||||||
|  |     { | ||||||
|  |         return new { | ||||||
|  |                        HostName           = Environment.MachineName | ||||||
|  |                      , CurrentConnections = SafetyShopHostMiddleware.Connections | ||||||
|  |                      , GlobalStatic.ProductVersion | ||||||
|  |                      , ThreadCounts = GlobalStatic.CurrentProcess.Threads.Count | ||||||
|  |                      , GlobalStatic.LatestLogTime | ||||||
|  |                    }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     系统是否已经安全停止 | ||||||
|  |     /// </summary> | ||||||
|  |     [AllowAnonymous] | ||||||
|  |     [HttpGet] | ||||||
|  |     [NonUnify] | ||||||
|  |     #pragma warning disable CA1822, S3400 | ||||||
|  |     public IActionResult IsSystemSafetyStopped(int logTimeoutSeconds = 15) | ||||||
|  |         #pragma warning restore S3400, CA1822 | ||||||
|  |     { | ||||||
|  |         return new ContentResult { Content = (DateTime.Now - GlobalStatic.LatestLogTime).TotalSeconds > logTimeoutSeconds ? "1" : "0" }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     实例下线 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <remarks> | ||||||
|  |     ///     流量只出不进 | ||||||
|  |     /// </remarks> | ||||||
|  |     [AllowAnonymous] | ||||||
|  |     [HttpGet] | ||||||
|  |     #pragma warning disable CA1822 | ||||||
|  |     public ActionResult Offline(string token) | ||||||
|  |         #pragma warning restore CA1822 | ||||||
|  |     { | ||||||
|  |         if (token != GlobalStatic.SecretKey) { | ||||||
|  |             return new UnauthorizedResult(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         SafetyShopHostMiddleware.Stop(); | ||||||
|  |         return new OkResult(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     停止日志计数器 | ||||||
|  |     /// </summary> | ||||||
|  |     [AllowAnonymous] | ||||||
|  |     [HttpGet] | ||||||
|  |     #pragma warning disable CA1822 | ||||||
|  |     public ActionResult StopLogCounter(string token) | ||||||
|  |         #pragma warning restore CA1822 | ||||||
|  |     { | ||||||
|  |         if (token != GlobalStatic.SecretKey) { | ||||||
|  |             return new UnauthorizedResult(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         GlobalStatic.LogCounterOff = true; | ||||||
|  |         return new OkResult(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | using NetAdmin.Application.Modules.Tpl; | ||||||
|  | using NetAdmin.Application.Services.Tpl.Dependency; | ||||||
|  | using NetAdmin.Cache.Tpl.Dependency; | ||||||
|  | using NetAdmin.Domain.Dto.Dependency; | ||||||
|  | using NetAdmin.Domain.Dto.Tpl.Example; | ||||||
|  | using NetAdmin.Host.Attributes; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Controllers.Tpl; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     示例服务 | ||||||
|  | /// </summary> | ||||||
|  | [ApiDescriptionSettings(nameof(Tpl), Module = nameof(Tpl))] | ||||||
|  | [Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)] | ||||||
|  | public sealed class ExampleController(IExampleCache cache) : ControllerBase<IExampleCache, IExampleService>(cache), IExampleModule | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     批量删除示例 | ||||||
|  |     /// </summary> | ||||||
|  |     [Transaction] | ||||||
|  |     public Task<int> BulkDeleteAsync(BulkReq<DelReq> req) | ||||||
|  |     { | ||||||
|  |         return Cache.BulkDeleteAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     示例计数 | ||||||
|  |     /// </summary> | ||||||
|  |     public Task<long> CountAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Cache.CountAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     创建示例 | ||||||
|  |     /// </summary> | ||||||
|  |     [Transaction] | ||||||
|  |     public Task<QueryExampleRsp> CreateAsync(CreateExampleReq req) | ||||||
|  |     { | ||||||
|  |         return Cache.CreateAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     删除示例 | ||||||
|  |     /// </summary> | ||||||
|  |     [Transaction] | ||||||
|  |     public Task<int> DeleteAsync(DelReq req) | ||||||
|  |     { | ||||||
|  |         return Cache.DeleteAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     示例是否存在 | ||||||
|  |     /// </summary> | ||||||
|  |     [NonAction] | ||||||
|  |     public Task<bool> ExistAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Cache.ExistAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     导出示例 | ||||||
|  |     /// </summary> | ||||||
|  |     [NonAction] | ||||||
|  |     public Task<IActionResult> ExportAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Cache.ExportAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取单个示例 | ||||||
|  |     /// </summary> | ||||||
|  |     public Task<QueryExampleRsp> GetAsync(QueryExampleReq req) | ||||||
|  |     { | ||||||
|  |         return Cache.GetAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     分页查询示例 | ||||||
|  |     /// </summary> | ||||||
|  |     public Task<PagedQueryRsp<QueryExampleRsp>> PagedQueryAsync(PagedQueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Cache.PagedQueryAsync(req); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     查询示例 | ||||||
|  |     /// </summary> | ||||||
|  |     [NonAction] | ||||||
|  |     public Task<IEnumerable<QueryExampleRsp>> QueryAsync(QueryReq<QueryExampleReq> req) | ||||||
|  |     { | ||||||
|  |         return Cache.QueryAsync(req); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | namespace NetAdmin.Host.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     HttpContext 扩展方法 | ||||||
|  | /// </summary> | ||||||
|  | public static class HttpContextExtensions | ||||||
|  | { | ||||||
|  |     private static readonly Regex _nullRegex = new("\"[^\"]+?\":null,?", RegexOptions.Compiled); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     删除 response json body 中value 为null的节点 | ||||||
|  |     /// </summary> | ||||||
|  |     public static async Task RemoveJsonNodeWithNullValueAsync(this HttpContext me) | ||||||
|  |     { | ||||||
|  |         // 非json格式,退出 | ||||||
|  |         if (!(me.Response.ContentType?.Contains(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON) ?? false)) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 流不可读,退出 | ||||||
|  |         if (!me.Response.Body.CanSeek || !me.Response.Body.CanRead || !me.Response.Body.CanWrite) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         _ = me.Response.Body.Seek(0, SeekOrigin.Begin); | ||||||
|  |         var sr         = new StreamReader(me.Response.Body); | ||||||
|  |         var bodyString = await sr.ReadToEndAsync().ConfigureAwait(false); | ||||||
|  |         bodyString = _nullRegex.Replace(bodyString, string.Empty).Replace(",}", "}"); | ||||||
|  |         _          = me.Response.Body.Seek(0, SeekOrigin.Begin); | ||||||
|  |         var bytes = Encoding.UTF8.GetBytes(bodyString); | ||||||
|  |         me.Response.Body.SetLength(bytes.Length); | ||||||
|  |         await me.Response.Body.WriteAsync(bytes).ConfigureAwait(false); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,62 @@ | |||||||
|  | #if DEBUG | ||||||
|  | using IGeekFan.AspNetCore.Knife4jUI; | ||||||
|  |  | ||||||
|  | #else | ||||||
|  | using Prometheus; | ||||||
|  | using Prometheus.HttpMetrics; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     ApplicationBuilder对象 扩展方法 | ||||||
|  | /// </summary> | ||||||
|  | [SuppressSniffer] | ||||||
|  |  | ||||||
|  | // ReSharper disable once InconsistentNaming | ||||||
|  | public static class IApplicationBuilderExtensions | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     执行匹配的端点 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IApplicationBuilder UseEndpoints(this IApplicationBuilder me) | ||||||
|  |     { | ||||||
|  |         return me.UseEndpoints(endpoints => { | ||||||
|  |             _ = endpoints.MapControllers(); | ||||||
|  |             #if !DEBUG | ||||||
|  |             _ = endpoints.MapMetrics(); | ||||||
|  |             #endif | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     #if DEBUG | ||||||
|  |     /// <summary> | ||||||
|  |     ///     使用 api skin (knife4j-vue) | ||||||
|  |     /// </summary> | ||||||
|  |     public static IApplicationBuilder UseOpenApiSkin(this IApplicationBuilder me) | ||||||
|  |     { | ||||||
|  |         return me.UseKnife4UI(options => { | ||||||
|  |             options.RoutePrefix = string.Empty; // 配置 Knife4UI 路由地址 | ||||||
|  |             foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) { | ||||||
|  |                 options.SwaggerEndpoint(groupInfo.RouteTemplate, groupInfo.Title); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     #else | ||||||
|  |     /// <summary> | ||||||
|  |     ///     使用 Prometheus | ||||||
|  |     /// </summary> | ||||||
|  |     public static IApplicationBuilder UsePrometheus(this IApplicationBuilder me) | ||||||
|  |     { | ||||||
|  |         return me.UseHttpMetrics(opt => { | ||||||
|  |             opt.RequestDuration.Histogram = Metrics.CreateHistogram( // | ||||||
|  |                 "http_request_duration_seconds" | ||||||
|  |               , "The duration of HTTP requests processed by an ASP.NET Core application." | ||||||
|  |               , HttpRequestLabelNames.All | ||||||
|  |               , new HistogramConfiguration { | ||||||
|  |                                                Buckets = Histogram.PowersOfTenDividedBuckets( | ||||||
|  |                                                    -2, 2, 4) | ||||||
|  |                                            }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     #endif | ||||||
|  | } | ||||||
| @@ -0,0 +1,115 @@ | |||||||
|  | using NetAdmin.Host.Filters; | ||||||
|  | using NetAdmin.Host.Utils; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     IMvcBuilder 扩展方法 | ||||||
|  | /// </summary> | ||||||
|  | [SuppressSniffer] | ||||||
|  |  | ||||||
|  | // ReSharper disable once InconsistentNaming | ||||||
|  | public static class IMvcBuilderExtensions | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     api结果处理器 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IMvcBuilder AddDefaultApiResultHandler(this IMvcBuilder me) | ||||||
|  |     { | ||||||
|  |         return me.AddInjectWithUnifyResult<DefaultApiResultHandler>(injectOptions => { | ||||||
|  |             injectOptions.ConfigureSwaggerGen(genOptions => { | ||||||
|  |                 // 替换自定义的EnumSchemaFilter,支持多语言Resx资源 (需将SpecificationDocumentSettings.EnableEnumSchemaFilter配置为false) | ||||||
|  |                 genOptions.SchemaFilter<SwaggerEnumSchemaFixer>(); | ||||||
|  |  | ||||||
|  |                 // 枚举显示自身xml comment 而不是$ref原型引用 | ||||||
|  |                 genOptions.UseInlineDefinitionsForEnums(); | ||||||
|  |  | ||||||
|  |                 // 将程序集版本号与OpenApi版本号同步 | ||||||
|  |                 foreach (var doc in genOptions.SwaggerGeneratorOptions.SwaggerDocs) { | ||||||
|  |                     doc.Value.Version = FileVersionInfo | ||||||
|  |                                         .GetVersionInfo(Assembly.GetEntryAssembly()!.Location) | ||||||
|  |                                         .ProductVersion; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Json序列化配置 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <remarks> | ||||||
|  |     ///     正反序列化规则: | ||||||
|  |     ///     object->json: | ||||||
|  |     ///     1、值为 null 或 default 的节点将被忽略 | ||||||
|  |     ///     2、值为 "" 的节点将被忽略。 | ||||||
|  |     ///     3、值为 [] 的节点将被忽略。 | ||||||
|  |     ///     4、节点名:大驼峰转小驼峰 | ||||||
|  |     ///     5、不转义除对json结构具有破坏性(如")以外的任何字符 | ||||||
|  |     ///     6、大数字原样输出(不加引号),由前端处理js大数兼容问题 | ||||||
|  |     ///     json->object: | ||||||
|  |     ///     1、允许带注释的json(自动忽略) | ||||||
|  |     ///     2、允许尾随逗号 | ||||||
|  |     ///     3、节点名大小写不敏感 | ||||||
|  |     ///     4、允许带双引号的数字 | ||||||
|  |     ///     5、值为"" 转 null | ||||||
|  |     ///     6、值为[] 转 null | ||||||
|  |     /// </remarks> | ||||||
|  |     public static IMvcBuilder AddJsonSerializer(this IMvcBuilder me, bool enumToString = false) | ||||||
|  |     { | ||||||
|  |         return me.AddJsonOptions(options => SetJsonOptions(enumToString, options)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     设置Json选项 | ||||||
|  |     /// </summary> | ||||||
|  |     private static void SetJsonOptions(bool enumToString, JsonOptions options) | ||||||
|  |     { | ||||||
|  |         ////////////////////////////// json -> object | ||||||
|  |  | ||||||
|  |         // 允许带注释 | ||||||
|  |         options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; | ||||||
|  |  | ||||||
|  |         // 允许尾随逗号 | ||||||
|  |         options.JsonSerializerOptions.AllowTrailingCommas = true; | ||||||
|  |  | ||||||
|  |         // 允许数字带双引号 | ||||||
|  |         options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; | ||||||
|  |  | ||||||
|  |         // 大小写不敏感 | ||||||
|  |         options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; | ||||||
|  |  | ||||||
|  |         // 允许读取引号包围的数字 | ||||||
|  |         options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; | ||||||
|  |  | ||||||
|  |         ///////////////////////////// object -> json | ||||||
|  |  | ||||||
|  |         // 转小驼峰 | ||||||
|  |         options.JsonSerializerOptions.DictionaryKeyPolicy  = JsonNamingPolicy.CamelCase; | ||||||
|  |         options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; | ||||||
|  |  | ||||||
|  |         // 不严格转义 | ||||||
|  |         options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; | ||||||
|  |  | ||||||
|  |         // 写入时,忽略null、default | ||||||
|  |         options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; | ||||||
|  |  | ||||||
|  |         ////////////////////////////// object <-> json | ||||||
|  |  | ||||||
|  |         // "" 转 null 双向 | ||||||
|  |         options.JsonSerializerOptions.Converters.Add(new ToNullIfEmptyStringConverter()); | ||||||
|  |  | ||||||
|  |         // [] 转 null 双向 | ||||||
|  |         options.JsonSerializerOptions.TypeInfoResolver = new CollectionJsonTypeInfoResolver(); | ||||||
|  |  | ||||||
|  |         // 日期格式 2023-01-18 20:02:12 | ||||||
|  |         _ = options.JsonSerializerOptions.Converters.AddDateTimeTypeConverters(); | ||||||
|  |  | ||||||
|  |         // object->json 枚举显名 而非数字 ,json->object 可以枚举名 也可以数值 | ||||||
|  |         if (enumToString) { | ||||||
|  |             options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 快捷访问方式 | ||||||
|  |         GlobalStatic.JsonSerializerOptions = options.JsonSerializerOptions; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace NetAdmin.Host.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Type 扩展方法 | ||||||
|  | /// </summary> | ||||||
|  | public static class MethodInfoExtensions | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取路由路径 | ||||||
|  |     /// </summary> | ||||||
|  |     public static string GetRoutePath(this MethodInfo me, IServiceProvider serviceProvider) | ||||||
|  |     { | ||||||
|  |         return serviceProvider.GetService<IActionDescriptorCollectionProvider>() | ||||||
|  |                               .ActionDescriptors.Items.FirstOrDefault(x => x.DisplayName!.StartsWith( // | ||||||
|  |                                                                           $"{me.DeclaringType}.{me.Name}", StringComparison.Ordinal)) | ||||||
|  |                               ?.AttributeRouteInfo?.Template; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | using NetAdmin.Domain.Dto; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     ResourceExecutingContextExtensions | ||||||
|  | /// </summary> | ||||||
|  | public static class ResourceExecutingContextExtensions | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     设置失败结果 | ||||||
|  |     /// </summary> | ||||||
|  |     public static void SetFailResult<T>(this ResourceExecutingContext me, ErrorCodes errorCode, string errorMsg = null) | ||||||
|  |         where T : RestfulInfo<object>, new() | ||||||
|  |     { | ||||||
|  |         me.Result                          = new JsonResult(new T { Code = errorCode, Msg = errorMsg }); | ||||||
|  |         me.HttpContext.Response.StatusCode = Numbers.HTTP_STATUS_BIZ_FAIL; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,216 @@ | |||||||
|  | using Gurion.Logging; | ||||||
|  | using NetAdmin.Domain.Contexts; | ||||||
|  | using StackExchange.Redis; | ||||||
|  | using Yitter.IdGenerator; | ||||||
|  | #if DEBUG | ||||||
|  | using Spectre.Console; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Host.Extensions; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     ServiceCollection 扩展方法 | ||||||
|  | /// </summary> | ||||||
|  | [SuppressSniffer] | ||||||
|  | public static class ServiceCollectionExtensions | ||||||
|  | { | ||||||
|  |     #if DEBUG | ||||||
|  |     private static readonly Dictionary<Regex, string> _consoleColors // | ||||||
|  |         = new() { | ||||||
|  |                     { | ||||||
|  |                         new Regex( // | ||||||
|  |                             @"(\d{2,}\.\d+ ?ms)", RegexOptions.Compiled) | ||||||
|  |                       , $"[{nameof(ConsoleColor.Magenta)}]$1[/]" | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(Tb[a-zA-Z0-9]+)", RegexOptions.Compiled) | ||||||
|  |                       , $"[{nameof(ConsoleColor.Cyan)}]$1[/]" | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(INSERT) ", RegexOptions.Compiled) | ||||||
|  |                       , $"[{nameof(ConsoleColor.Blue)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(SELECT) ", RegexOptions.Compiled) | ||||||
|  |                       , $"[{nameof(ConsoleColor.Green)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(UPDATE) ", RegexOptions.Compiled) | ||||||
|  |                       , $"[{nameof(ConsoleColor.Yellow)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(DELETE) ", RegexOptions.Compiled) | ||||||
|  |                       , $"[{nameof(ConsoleColor.Red)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(<s:.+?>)", RegexOptions.Compiled) | ||||||
|  |                       , $"[underline {nameof(ConsoleColor.Gray)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(ResponseBody)", RegexOptions.Compiled) | ||||||
|  |                       , $"[underline {nameof(ConsoleColor.Cyan)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             "(RequestBody)", RegexOptions.Compiled) | ||||||
|  |                       , $"[underline {nameof(ConsoleColor.Magenta)}]$1[/] " | ||||||
|  |                     } | ||||||
|  |                   , { | ||||||
|  |                         new Regex( // | ||||||
|  |                             @"(\[\[dbo\]\]\.)(\[\[.+?\]\]) ", RegexOptions.Compiled) | ||||||
|  |                       , $"$1[{nameof(ConsoleColor.Magenta)}]$2[/] " | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |     #endif | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     扫描程序集中继承自IConfigurableOptions的选项,注册 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddAllOptions( // | ||||||
|  |         this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         var optionsTypes | ||||||
|  |             = from type in App.EffectiveTypes.Where(x => !x.IsAbstract && !x.FullName!.Contains(nameof(Gurion)) && | ||||||
|  |                                                          x.GetInterfaces().Contains(typeof(IConfigurableOptions))) | ||||||
|  |               select type; | ||||||
|  |  | ||||||
|  |         var sbLog = new StringBuilder(); | ||||||
|  |         foreach (var type in optionsTypes) { | ||||||
|  |             var configureMethod = typeof(ConfigurableOptionsServiceCollectionExtensions).GetMethod( | ||||||
|  |                 nameof(ConfigurableOptionsServiceCollectionExtensions.AddConfigurableOptions), BindingFlags.Public | BindingFlags.Static | ||||||
|  |               , [typeof(IServiceCollection)]); | ||||||
|  |             _ = configureMethod!.MakeGenericMethod(type).Invoke(me, [me]); | ||||||
|  |             _ = sbLog.Append(CultureInfo.InvariantCulture, $" {type.Name}"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         LogHelper.Get<IServiceCollection>()?.Info($"{Ln.配置文件初始化完毕} {sbLog}"); | ||||||
|  |         return me; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加控制台日志模板 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddConsoleFormatter(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         return me.AddConsoleFormatter(options => { | ||||||
|  |             var logLevels = Enum.GetValues<LogLevels>().ToDictionary(x => x, x => x.GetDisplay()); | ||||||
|  |  | ||||||
|  |             options.WriteHandler = (message, _, _, _, _) => { | ||||||
|  |                 #if DEBUG | ||||||
|  |                 MarkupLine(message.Message.EscapeMarkup(), message, logLevels); | ||||||
|  |                 if (message.Exception != null) { | ||||||
|  |                     MarkupLine(message.Exception.ToString().EscapeMarkup(), message, logLevels); | ||||||
|  |                 } | ||||||
|  |                 #else | ||||||
|  |                 var msg = message.Message.ReplaceLineEndings(string.Empty); | ||||||
|  |                 var (date, logName, logFormat) = ParseMessage(message, false); | ||||||
|  |                 Console.WriteLine( // | ||||||
|  |                     logFormat, date, logLevels[(LogLevels)message.LogLevel].ShortName, logName, message.ThreadId, msg); | ||||||
|  |                 #endif | ||||||
|  |                 GlobalStatic.IncrementLogCounter(); | ||||||
|  |             }; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加上下文用户令牌 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddContextUserToken(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         return me.AddScoped(typeof(ContextUserToken), _ => ContextUserToken.Create()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加事件总线 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddEventBus(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         return me.AddEventBus(builder => builder.AddSubscribers(App.Assemblies.ToArray())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加内存缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddMemCache(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         return me.AddMemoryCache(options => options.TrackStatistics = true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     OpenTelemetry数据监控 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddOpenTelemetryNet(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         // _ = me.AddOpenTelemetry() | ||||||
|  |         //       .WithMetrics(builder => builder.AddAspNetCoreInstrumentation() | ||||||
|  |         //                                      .AddHttpClientInstrumentation() | ||||||
|  |         //                                      .AddRuntimeInstrumentation() | ||||||
|  |         //                                      .AddProcessInstrumentation() | ||||||
|  |         //                                      .AddPrometheusExporter()); | ||||||
|  |         return me; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加 Redis缓存 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddRedisCache(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         var redisOptions = App.GetOptions<RedisOptions>().Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE); | ||||||
|  |  | ||||||
|  |         // IDistributedCache 分布式缓存通用接口 | ||||||
|  |         _ = me.AddStackExchangeRedisCache(options => { | ||||||
|  |             // 连接字符串 | ||||||
|  |             options.Configuration = redisOptions.ConnStr; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // Redis原生接口 | ||||||
|  |         return me.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisOptions.ConnStr)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     添加雪花编号生成器 | ||||||
|  |     /// </summary> | ||||||
|  |     public static IServiceCollection AddSnowflake(this IServiceCollection me) | ||||||
|  |     { | ||||||
|  |         // 雪花漂移算法 | ||||||
|  |         var workerId           = Environment.GetEnvironmentVariable(Chars.FLG_SNOWFLAKE_WORK_ID).Int32Try(0); | ||||||
|  |         var idGeneratorOptions = new IdGeneratorOptions((ushort)workerId) { WorkerIdBitLength = 6 }; | ||||||
|  |         YitIdHelper.SetIdGenerator(idGeneratorOptions); | ||||||
|  |         return me; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #if DEBUG | ||||||
|  |     private static void MarkupLine(                     // | ||||||
|  |         string                                  msg     // | ||||||
|  |       , LogMessage                              message // | ||||||
|  |       , Dictionary<LogLevels, DisplayAttribute> logLevels) | ||||||
|  |     { | ||||||
|  |         msg = _consoleColors.Aggregate( // | ||||||
|  |             msg, (current, regex) => regex.Key.Replace(current, regex.Value)); | ||||||
|  |         msg = msg.ReplaceLineEndings(string.Empty); | ||||||
|  |         var colorName = logLevels[(LogLevels)message.LogLevel].Name!; | ||||||
|  |         var (date, logName, logFormat) = ParseMessage(message, true); | ||||||
|  |         AnsiConsole.MarkupLine( // | ||||||
|  |             CultureInfo.InvariantCulture, logFormat, date, colorName, logName, message.ThreadId, msg); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #endif | ||||||
|  |     private static (string Date, string LogName, string LogFormat) ParseMessage(LogMessage message, bool showColor) | ||||||
|  |     { | ||||||
|  |         var date    = $"{message.LogDateTime:HH:mm:ss.ffffff}"; | ||||||
|  |         var logName = message.LogName.PadRight(64, ' ')[^64..]; | ||||||
|  |         var format = showColor | ||||||
|  |             ? $"[{nameof(ConsoleColor.Gray)}][[{{0}} {{1}} {{2,-{64}}} #{{3,4}}]][/] {{4}}" | ||||||
|  |             : $"[{{0}} {{1}} {{2,-{64}}} #{{3,4}}] {{4}}"; | ||||||
|  |  | ||||||
|  |         return (date, logName, format); | ||||||
|  |     } | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 GitHub
						GitHub