mirror of
				https://github.com/nsnail/NetAdmin.git
				synced 2025-11-04 13:10:50 +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