diff --git a/src/backend/NetAdmin/NetAdmin.Application/Extensions/UnitOfWorkManagerExtensions.cs b/src/backend/NetAdmin/NetAdmin.Application/Extensions/UnitOfWorkManagerExtensions.cs
new file mode 100644
index 00000000..2d2285db
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Extensions/UnitOfWorkManagerExtensions.cs
@@ -0,0 +1,31 @@
+namespace NetAdmin.Application.Extensions;
+
+///
+/// 工作单元管理器扩展方法
+///
+public static class UnitOfWorkManagerExtensions
+{
+ ///
+ /// 事务操作
+ ///
+ public static async Task AtomicOperateAsync(this UnitOfWorkManager me, Func handle)
+ {
+ var logger = LogHelper.Get();
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Modules/ICrudModule.cs b/src/backend/NetAdmin/NetAdmin.Application/Modules/ICrudModule.cs
new file mode 100644
index 00000000..441b4368
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Modules/ICrudModule.cs
@@ -0,0 +1,65 @@
+using NetAdmin.Domain;
+using NetAdmin.Domain.Dto.Dependency;
+
+namespace NetAdmin.Application.Modules;
+
+///
+/// 增删改查模块接口
+///
+/// 创建请求类型
+/// 创建响应类型
+/// 查询请求类型
+/// 查询响应类型
+/// 删除请求类型
+public interface ICrudModule
+ where TCreateReq : DataAbstraction, new()
+ where TCreateRsp : DataAbstraction
+ where TQueryReq : DataAbstraction, new()
+ where TQueryRsp : DataAbstraction
+ where TDelReq : DataAbstraction, new()
+{
+ ///
+ /// 批量删除实体
+ ///
+ Task BulkDeleteAsync(BulkReq req);
+
+ ///
+ /// 实体计数
+ ///
+ Task CountAsync(QueryReq req);
+
+ ///
+ /// 创建实体
+ ///
+ Task CreateAsync(TCreateReq req);
+
+ ///
+ /// 删除实体
+ ///
+ Task DeleteAsync(TDelReq req);
+
+ ///
+ /// 判断实体是否存在
+ ///
+ Task ExistAsync(QueryReq req);
+
+ ///
+ /// 导出实体
+ ///
+ Task ExportAsync(QueryReq req);
+
+ ///
+ /// 获取单个实体
+ ///
+ Task GetAsync(TQueryReq req);
+
+ ///
+ /// 分页查询实体
+ ///
+ Task> PagedQueryAsync(PagedQueryReq req);
+
+ ///
+ /// 查询实体
+ ///
+ Task> QueryAsync(QueryReq req);
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Modules/Tpl/IExampleModule.cs b/src/backend/NetAdmin/NetAdmin.Application/Modules/Tpl/IExampleModule.cs
new file mode 100644
index 00000000..fd653293
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Modules/Tpl/IExampleModule.cs
@@ -0,0 +1,12 @@
+using NetAdmin.Domain.Dto.Dependency;
+using NetAdmin.Domain.Dto.Tpl.Example;
+
+namespace NetAdmin.Application.Modules.Tpl;
+
+///
+/// 示例模块
+///
+public interface IExampleModule : ICrudModule;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/NetAdmin.Application.csproj b/src/backend/NetAdmin/NetAdmin.Application/NetAdmin.Application.csproj
new file mode 100644
index 00000000..c21da26e
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/NetAdmin.Application.csproj
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Repositories/BasicRepository.cs b/src/backend/NetAdmin/NetAdmin.Application/Repositories/BasicRepository.cs
new file mode 100644
index 00000000..779faee2
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Repositories/BasicRepository.cs
@@ -0,0 +1,18 @@
+using NetAdmin.Domain.Contexts;
+using NetAdmin.Domain.DbMaps.Dependency;
+
+namespace NetAdmin.Application.Repositories;
+
+///
+/// 基础仓储
+///
+public sealed class BasicRepository(IFreeSql fSql, UnitOfWorkManager uowManger, ContextUserToken userToken)
+ : DefaultRepository(fSql, uowManger)
+ where TEntity : EntityBase //
+ where TPrimary : IEquatable
+{
+ ///
+ /// 当前上下文关联的用户令牌
+ ///
+ public ContextUserToken UserToken => userToken;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Services/IService.cs b/src/backend/NetAdmin/NetAdmin.Application/Services/IService.cs
new file mode 100644
index 00000000..48bc95a2
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Services/IService.cs
@@ -0,0 +1,19 @@
+using NetAdmin.Domain.Contexts;
+
+namespace NetAdmin.Application.Services;
+
+///
+/// 服务接口
+///
+public interface IService
+{
+ ///
+ /// 服务编号
+ ///
+ Guid ServiceId { get; init; }
+
+ ///
+ /// 上下文用户令牌
+ ///
+ ContextUserToken UserToken { get; set; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Services/RedisService.cs b/src/backend/NetAdmin/NetAdmin.Application/Services/RedisService.cs
new file mode 100644
index 00000000..dc7c304b
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Services/RedisService.cs
@@ -0,0 +1,43 @@
+using NetAdmin.Application.Repositories;
+using NetAdmin.Domain.DbMaps.Dependency;
+using StackExchange.Redis;
+
+namespace NetAdmin.Application.Services;
+
+///
+/// Redis Service Base
+///
+///
+/// Initializes a new instance of the class.
+/// Redis Service Base
+///
+public abstract class RedisService(BasicRepository rpo)
+ : RepositoryService(rpo)
+ where TEntity : EntityBase //
+ where TPrimary : IEquatable
+{
+ ///
+ /// Redis Database
+ ///
+ protected IDatabase RedisDatabase { get; } //
+ = App.GetService()
+ .GetDatabase(App.GetOptions().Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE).Database);
+
+ ///
+ /// 获取锁
+ ///
+ protected Task 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));
+ }
+
+ ///
+ /// 获取锁(仅获取一次)
+ ///
+ protected Task GetLockerOnceAsync(string lockerName)
+ {
+ return RedisLocker.GetLockerAsync(RedisDatabase, lockerName, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), 1
+ , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY));
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Services/RepositoryService.cs b/src/backend/NetAdmin/NetAdmin.Application/Services/RepositoryService.cs
new file mode 100644
index 00000000..7209bc07
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Services/RepositoryService.cs
@@ -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;
+
+///
+/// 仓储服务基类
+///
+/// 实体类型
+/// 主键类型
+/// 日志类型
+public abstract class RepositoryService(BasicRepository rpo) : ServiceBase
+ where TEntity : EntityBase //
+ where TPrimary : IEquatable
+{
+ ///
+ /// 默认仓储
+ ///
+ protected BasicRepository Rpo => rpo;
+
+ ///
+ /// 启用级联保存
+ ///
+ protected bool EnableCascadeSave {
+ get => Rpo.DbContextOptions.EnableCascadeSave;
+ set => Rpo.DbContextOptions.EnableCascadeSave = value;
+ }
+
+ ///
+ /// 导出实体
+ ///
+ protected static async Task ExportAsync( //
+ Func, ISelectGrouping> selector, QueryReq query, string fileName
+ , Expression, object>> listExp = null)
+ where TQuery : DataAbstraction, new()
+ {
+ var list = await selector(query).Take(Numbers.MAX_LIMIT_EXPORT).ToListAsync(listExp).ConfigureAwait(false);
+ return await GetExportFileStreamAsync(fileName, list).ConfigureAwait(false);
+ }
+
+ ///
+ /// 导出实体
+ ///
+ protected static async Task ExportAsync( //
+ Func, ISelect> selector, QueryReq query, string fileName, Expression> 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(fileName, list).ConfigureAwait(false);
+ }
+
+ ///
+ /// 更新实体
+ ///
+ /// 新的值
+ /// 包含的属性
+ /// 排除的属性
+ /// 查询表达式
+ /// 查询sql
+ /// 是否忽略版本锁
+ /// 更新行数
+ protected Task UpdateAsync( //
+ TEntity newValue //
+ , IEnumerable includeFields //
+ , string[] excludeFields = null //
+ , Expression> 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
+ ///
+ /// 更新实体
+ ///
+ /// 新的值
+ /// 包含的属性
+ /// 排除的属性
+ /// 查询表达式
+ /// 查询sql
+ /// 是否忽略版本锁
+ /// 更新后的实体列表
+ protected Task> UpdateReturnListAsync( //
+ TEntity newValue //
+ , IEnumerable includeFields //
+ , string[] excludeFields = null //
+ , Expression> 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 GetExportFileStreamAsync(string fileName, object list)
+ {
+ var listTyped = list.Adapt>();
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream);
+ var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
+ csv.WriteHeader();
+ 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 BuildUpdate( //
+ TEntity entity //
+ , IEnumerable 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;
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Services/ServiceBase.cs b/src/backend/NetAdmin/NetAdmin.Application/Services/ServiceBase.cs
new file mode 100644
index 00000000..174c0e0e
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Services/ServiceBase.cs
@@ -0,0 +1,41 @@
+using NetAdmin.Domain.Contexts;
+
+namespace NetAdmin.Application.Services;
+
+///
+public abstract class ServiceBase : ServiceBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected ServiceBase() //
+ {
+ Logger = App.GetService>();
+ }
+
+ ///
+ /// 日志记录器
+ ///
+ protected ILogger Logger { get; }
+}
+
+///
+/// 服务基类
+///
+public abstract class ServiceBase : IScoped, IService
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected ServiceBase()
+ {
+ UserToken = App.GetService();
+ ServiceId = Guid.NewGuid();
+ }
+
+ ///
+ public Guid ServiceId { get; init; }
+
+ ///
+ public ContextUserToken UserToken { get; set; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Services/Tpl/Dependency/IExampleService.cs b/src/backend/NetAdmin/NetAdmin.Application/Services/Tpl/Dependency/IExampleService.cs
new file mode 100644
index 00000000..670d0f04
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Services/Tpl/Dependency/IExampleService.cs
@@ -0,0 +1,8 @@
+using NetAdmin.Application.Modules.Tpl;
+
+namespace NetAdmin.Application.Services.Tpl.Dependency;
+
+///
+/// 示例服务
+///
+public interface IExampleService : IService, IExampleModule;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Application/Services/Tpl/ExampleService.cs b/src/backend/NetAdmin/NetAdmin.Application/Services/Tpl/ExampleService.cs
new file mode 100644
index 00000000..bf23ecea
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Application/Services/Tpl/ExampleService.cs
@@ -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;
+
+///
+public sealed class ExampleService(BasicRepository rpo) //
+ : RepositoryService(rpo), IExampleService
+{
+ ///
+ public async Task BulkDeleteAsync(BulkReq req)
+ {
+ req.ThrowIfInvalid();
+ var ret = 0;
+
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ foreach (var item in req.Items) {
+ ret += await DeleteAsync(item).ConfigureAwait(false);
+ }
+
+ return ret;
+ }
+
+ ///
+ public Task CountAsync(QueryReq req)
+ {
+ req.ThrowIfInvalid();
+ return QueryInternal(req)
+ #if DBTYPE_SQLSERVER
+ .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
+ #endif
+ .CountAsync();
+ }
+
+ ///
+ public async Task CreateAsync(CreateExampleReq req)
+ {
+ req.ThrowIfInvalid();
+ var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
+ return ret.Adapt();
+ }
+
+ ///
+ public Task DeleteAsync(DelReq req)
+ {
+ req.ThrowIfInvalid();
+ return Rpo.DeleteAsync(a => a.Id == req.Id);
+ }
+
+ ///
+ public Task ExistAsync(QueryReq req)
+ {
+ req.ThrowIfInvalid();
+ return QueryInternal(req)
+ #if DBTYPE_SQLSERVER
+ .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
+ #endif
+ .AnyAsync();
+ }
+
+ ///
+ public Task ExportAsync(QueryReq req)
+ {
+ req.ThrowIfInvalid();
+ return ExportAsync(QueryInternal, req, Ln.示例导出);
+ }
+
+ ///
+ public async Task GetAsync(QueryExampleReq req)
+ {
+ req.ThrowIfInvalid();
+ var ret = await QueryInternal(new QueryReq { Filter = req, Order = Orders.None }).ToOneAsync().ConfigureAwait(false);
+ return ret.Adapt();
+ }
+
+ ///
+ public async Task> PagedQueryAsync(PagedQueryReq 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(req.Page, req.PageSize, total, list.Adapt>());
+ }
+
+ ///
+ public async Task> QueryAsync(QueryReq 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>();
+ }
+
+ private ISelect QueryInternal(QueryReq 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;
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/CacheBase.cs b/src/backend/NetAdmin/NetAdmin.Cache/CacheBase.cs
new file mode 100644
index 00000000..c05f2660
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/CacheBase.cs
@@ -0,0 +1,16 @@
+using NetAdmin.Application.Services;
+
+namespace NetAdmin.Cache;
+
+///
+/// 缓存基类
+///
+public abstract class CacheBase(TCacheContainer cache, TService service) : ICache
+ where TService : IService
+{
+ ///
+ public TCacheContainer Cache => cache;
+
+ ///
+ public TService Service => service;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/DistributedCache.cs b/src/backend/NetAdmin/NetAdmin.Cache/DistributedCache.cs
new file mode 100644
index 00000000..50ce7975
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/DistributedCache.cs
@@ -0,0 +1,93 @@
+using System.Runtime.CompilerServices;
+using NetAdmin.Application.Services;
+
+namespace NetAdmin.Cache;
+
+///
+/// 分布式缓存
+///
+public abstract class DistributedCache(IDistributedCache cache, TService service) : CacheBase(cache, service)
+ where TService : IService
+{
+ ///
+ /// 创建缓存
+ ///
+ /// 缓存键
+ /// 创建对象
+ /// 绝对过期时间
+ /// 滑动过期时间
+ /// 缓存对象类型
+ /// 缓存对象
+ protected Task CreateAsync(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);
+ }
+
+ ///
+ /// 获取缓存
+ ///
+ protected async Task GetAsync(string key)
+ {
+ var cacheRead = await Cache.GetStringAsync(key).ConfigureAwait(false);
+ try {
+ return cacheRead != null ? cacheRead.ToObject() : default;
+ }
+ catch (JsonException) {
+ return default;
+ }
+ }
+
+ ///
+ /// 获取缓存键
+ ///
+ protected string GetCacheKey(string id = "0", [CallerMemberName] string memberName = null)
+ {
+ return $"{GetType().FullName}.{memberName}.{id}";
+ }
+
+ ///
+ /// 获取或创建缓存
+ ///
+ /// 缓存键
+ /// 创建函数
+ /// 绝对过期时间
+ /// 滑动过期时间
+ /// 缓存对象类型
+ /// 缓存对象
+ protected async Task GetOrCreateAsync(string key, Func> createProc, TimeSpan? absLifeTime = null, TimeSpan? slideLifeTime = null)
+ {
+ var cacheRead = await GetAsync(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;
+ }
+
+ ///
+ /// 删除缓存
+ ///
+ protected Task RemoveAsync(string key)
+ {
+ return Cache.RemoveAsync(key);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/ICache.cs b/src/backend/NetAdmin/NetAdmin.Cache/ICache.cs
new file mode 100644
index 00000000..092f8efd
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/ICache.cs
@@ -0,0 +1,20 @@
+using NetAdmin.Application.Services;
+
+namespace NetAdmin.Cache;
+
+///
+/// 缓存接口
+///
+public interface ICache
+ where TService : IService
+{
+ ///
+ /// 缓存对象
+ ///
+ TCacheLoad Cache { get; }
+
+ ///
+ /// 关联的服务
+ ///
+ public TService Service { get; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/MemoryCache.cs b/src/backend/NetAdmin/NetAdmin.Cache/MemoryCache.cs
new file mode 100644
index 00000000..af232fbc
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/MemoryCache.cs
@@ -0,0 +1,9 @@
+using NetAdmin.Application.Services;
+
+namespace NetAdmin.Cache;
+
+///
+/// 内存缓存
+///
+public abstract class MemoryCache(IMemoryCache cache, TService service) : CacheBase(cache, service)
+ where TService : IService;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/NetAdmin.Cache.csproj b/src/backend/NetAdmin/NetAdmin.Cache/NetAdmin.Cache.csproj
new file mode 100644
index 00000000..6138b0be
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/NetAdmin.Cache.csproj
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/Tpl/Dependency/IExampleCache.cs b/src/backend/NetAdmin/NetAdmin.Cache/Tpl/Dependency/IExampleCache.cs
new file mode 100644
index 00000000..d887f95a
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/Tpl/Dependency/IExampleCache.cs
@@ -0,0 +1,9 @@
+using NetAdmin.Application.Modules.Tpl;
+using NetAdmin.Application.Services.Tpl.Dependency;
+
+namespace NetAdmin.Cache.Tpl.Dependency;
+
+///
+/// 示例缓存
+///
+public interface IExampleCache : ICache, IExampleModule;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Cache/Tpl/ExampleCache.cs b/src/backend/NetAdmin/NetAdmin.Cache/Tpl/ExampleCache.cs
new file mode 100644
index 00000000..d2a7b472
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Cache/Tpl/ExampleCache.cs
@@ -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;
+
+///
+public sealed class ExampleCache(IDistributedCache cache, IExampleService service)
+ : DistributedCache(cache, service), IScoped, IExampleCache
+{
+ ///
+ public Task BulkDeleteAsync(BulkReq req)
+ {
+ return Service.BulkDeleteAsync(req);
+ }
+
+ ///
+ public Task CountAsync(QueryReq req)
+ {
+ return Service.CountAsync(req);
+ }
+
+ ///
+ public Task CreateAsync(CreateExampleReq req)
+ {
+ return Service.CreateAsync(req);
+ }
+
+ ///
+ public Task DeleteAsync(DelReq req)
+ {
+ return Service.DeleteAsync(req);
+ }
+
+ ///
+ public Task ExistAsync(QueryReq req)
+ {
+ return Service.ExistAsync(req);
+ }
+
+ ///
+ public Task ExportAsync(QueryReq req)
+ {
+ return Service.ExportAsync(req);
+ }
+
+ ///
+ public Task GetAsync(QueryExampleReq req)
+ {
+ return Service.GetAsync(req);
+ }
+
+ ///
+ public Task> PagedQueryAsync(PagedQueryReq req)
+ {
+ return Service.PagedQueryAsync(req);
+ }
+
+ ///
+ public Task> QueryAsync(QueryReq req)
+ {
+ return Service.QueryAsync(req);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DangerFieldAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DangerFieldAttribute.cs
new file mode 100644
index 00000000..63deb7c0
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DangerFieldAttribute.cs
@@ -0,0 +1,7 @@
+namespace NetAdmin.Domain.Attributes;
+
+///
+/// 危险字段标记
+///
+[AttributeUsage(AttributeTargets.Property)]
+public sealed class DangerFieldAttribute : Attribute;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/AlipayAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/AlipayAttribute.cs
new file mode 100644
index 00000000..47b7dfef
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/AlipayAttribute.cs
@@ -0,0 +1,23 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 支付宝验证器(手机或邮箱)
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class AlipayAttribute : ValidationAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AlipayAttribute()
+ {
+ ErrorMessageResourceName = nameof(Ln.支付宝账号);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+
+ ///
+ public override bool IsValid(object value)
+ {
+ return new MobileAttribute().IsValid(value) || new EmailAddressAttribute().IsValid(value);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/CertificateAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/CertificateAttribute.cs
new file mode 100644
index 00000000..3c70b01a
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/CertificateAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 证件号码验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class CertificateAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CertificateAttribute() //
+ : base(Chars.RGX_CERTIFICATE)
+ {
+ ErrorMessageResourceName = nameof(Ln.无效证件号码);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/ChineseNameAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/ChineseNameAttribute.cs
new file mode 100644
index 00000000..c1609be0
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/ChineseNameAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 中文姓名验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class ChineseNameAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChineseNameAttribute() //
+ : base(Chars.RGXL_CHINESE_NAME)
+ {
+ ErrorMessageResourceName = nameof(Ln.中文姓名);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/CronAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/CronAttribute.cs
new file mode 100644
index 00000000..38d40ca5
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/CronAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 时间表达式验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class CronAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CronAttribute() //
+ : base(Chars.RGXL_CRON)
+ {
+ ErrorMessageResourceName = nameof(Ln.时间表达式);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/EmailAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/EmailAttribute.cs
new file mode 100644
index 00000000..0573aaa2
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/EmailAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 邮箱验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class EmailAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EmailAttribute() //
+ : base(Chars.RGXL_EMAIL)
+ {
+ ErrorMessageResourceName = nameof(Ln.电子邮箱);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/InviteCodeAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/InviteCodeAttribute.cs
new file mode 100644
index 00000000..a55a02ec
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/InviteCodeAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 邀请码验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class InviteCodeAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InviteCodeAttribute() //
+ : base(Chars.RGX_INVITE_CODE)
+ {
+ ErrorMessageResourceName = nameof(Ln.邀请码不正确);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/JsonStringAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/JsonStringAttribute.cs
new file mode 100644
index 00000000..f46eb3aa
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/JsonStringAttribute.cs
@@ -0,0 +1,14 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// JSON文本验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class JsonStringAttribute : ValidationAttribute
+{
+ ///
+ protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+ {
+ return (value as string).IsJsonString() ? ValidationResult.Success : new ValidationResult(Ln.非JSON字符串, [validationContext.MemberName]);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/MobileAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/MobileAttribute.cs
new file mode 100644
index 00000000..0d550f0d
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/MobileAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 手机号码验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class MobileAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MobileAttribute() //
+ : base(Chars.RGX_MOBILE)
+ {
+ ErrorMessageResourceName = nameof(Ln.手机号码不正确);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PasswordAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PasswordAttribute.cs
new file mode 100644
index 00000000..0ff71759
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PasswordAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 密码验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class PasswordAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PasswordAttribute() //
+ : base(Chars.RGX_PASSWORD)
+ {
+ ErrorMessageResourceName = nameof(Ln._8位以上数字字母组合);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PayPasswordAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PayPasswordAttribute.cs
new file mode 100644
index 00000000..f97f1c5c
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PayPasswordAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 交易密码验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class PayPasswordAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PayPasswordAttribute() //
+ : base(Chars.RGX_PAY_PASSWORD)
+ {
+ ErrorMessageResourceName = nameof(Ln._6位数字);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PortAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PortAttribute.cs
new file mode 100644
index 00000000..36d55df8
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/PortAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 端口号验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class PortAttribute : RangeAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PortAttribute() //
+ : base(1, ushort.MaxValue)
+ {
+ ErrorMessageResourceName = nameof(Ln.无效端口号);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/RegexAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/RegexAttribute.cs
new file mode 100644
index 00000000..d00e62c5
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/RegexAttribute.cs
@@ -0,0 +1,16 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 正则表达式验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+#pragma warning disable DesignedForInheritance
+public class RegexAttribute : RegularExpressionAttribute
+#pragma warning restore DesignedForInheritance
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected RegexAttribute(string pattern) //
+ : base(pattern) { }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/TelephoneAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/TelephoneAttribute.cs
new file mode 100644
index 00000000..c521e97a
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/TelephoneAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 固定电话验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class TelephoneAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TelephoneAttribute() //
+ : base(Chars.RGX_TELEPHONE)
+ {
+ ErrorMessageResourceName = nameof(Ln.区号电话号码分机号);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/UserNameAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/UserNameAttribute.cs
new file mode 100644
index 00000000..ca17b04a
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/UserNameAttribute.cs
@@ -0,0 +1,34 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 用户名验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class UserNameAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UserNameAttribute() //
+ : base(Chars.RGX_USERNAME)
+ {
+ ErrorMessageResourceType = typeof(Ln);
+ }
+
+ ///
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/VerifyCodeAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/VerifyCodeAttribute.cs
new file mode 100644
index 00000000..07cdf1c1
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/DataValidation/VerifyCodeAttribute.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Domain.Attributes.DataValidation;
+
+///
+/// 验证码验证器
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
+public sealed class VerifyCodeAttribute : RegexAttribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public VerifyCodeAttribute() //
+ : base(Chars.RGX_VERIFY_CODE)
+ {
+ ErrorMessageResourceName = nameof(Ln.验证码不正确);
+ ErrorMessageResourceType = typeof(Ln);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/IndicatorAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/IndicatorAttribute.cs
new file mode 100644
index 00000000..f489eb59
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/IndicatorAttribute.cs
@@ -0,0 +1,14 @@
+namespace NetAdmin.Domain.Attributes;
+
+///
+/// 标记一个枚举的状态指示
+///
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Enum)]
+public sealed class IndicatorAttribute(string indicate) : Attribute
+{
+ ///
+ /// 状态指示
+ ///
+ public string Indicate { get; } = indicate;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/ServerTimeAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/ServerTimeAttribute.cs
new file mode 100644
index 00000000..e480bfa0
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/ServerTimeAttribute.cs
@@ -0,0 +1,7 @@
+namespace NetAdmin.Domain.Attributes;
+
+///
+/// 标记一个字段启用服务器时间
+///
+[AttributeUsage(AttributeTargets.Property)]
+public sealed class ServerTimeAttribute : Attribute;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Attributes/SnowflakeAttribute.cs b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/SnowflakeAttribute.cs
new file mode 100644
index 00000000..84578e15
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Attributes/SnowflakeAttribute.cs
@@ -0,0 +1,9 @@
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+
+namespace NetAdmin.Domain.Attributes;
+
+///
+/// 标记一个字段启用雪花编号生成
+///
+[AttributeUsage(AttributeTargets.Property)]
+public sealed class SnowflakeAttribute : Attribute;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Contexts/ContextUserToken.cs b/src/backend/NetAdmin/NetAdmin.Domain/Contexts/ContextUserToken.cs
new file mode 100644
index 00000000..6ec5e9cb
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Contexts/ContextUserToken.cs
@@ -0,0 +1,58 @@
+namespace NetAdmin.Domain.Contexts;
+
+///
+/// 上下文用户凭据
+///
+public sealed record ContextUserToken : DataAbstraction
+{
+ ///
+ /// 部门编号
+ ///
+ /// ReSharper disable once MemberCanBePrivate.Global
+ public long DeptId { get; init; }
+
+ ///
+ /// 用户编号
+ ///
+ /// ReSharper disable once MemberCanBePrivate.Global
+ public long Id { get; init; }
+
+ ///
+ /// 做授权验证的Token,全局唯一,可以随时重置(强制下线)
+ ///
+ /// ReSharper disable once MemberCanBePrivate.Global
+ public Guid Token { get; init; }
+
+ ///
+ /// 用户名
+ ///
+ /// ReSharper disable once MemberCanBePrivate.Global
+ public string UserName { get; init; }
+
+ ///
+ /// 从HttpContext 创建上下文用户
+ ///
+ public static ContextUserToken Create()
+ {
+ var claim = App.User?.FindFirst(nameof(ContextUserToken));
+ return claim?.Value.ToObject();
+ }
+
+ ///
+ /// 从 QueryUserRsp 创建上下文用户
+ ///
+ public static ContextUserToken Create(long id, Guid token, string userName, long deptId)
+ {
+ return new ContextUserToken { Id = id, Token = token, UserName = userName, DeptId = deptId };
+ }
+
+ ///
+ /// 从 Json Web Token 创建上下文用户
+ ///
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DataAbstraction.cs b/src/backend/NetAdmin/NetAdmin.Domain/DataAbstraction.cs
new file mode 100644
index 00000000..51ec5107
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DataAbstraction.cs
@@ -0,0 +1,48 @@
+namespace NetAdmin.Domain;
+
+///
+/// 数据基类
+///
+public abstract record DataAbstraction
+{
+ ///
+ /// 如果数据校验失败,抛出异常
+ ///
+ /// NetAdminValidateException
+ public void ThrowIfInvalid()
+ {
+ var validationResult = this.TryValidate();
+ if (!validationResult.IsValid) {
+ throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( //
+ x => x.MemberNames.First() //
+ , x => new[] { x.ErrorMessage }));
+ }
+ }
+
+ ///
+ public override string ToString()
+ {
+ return this.ToJson();
+ }
+
+ ///
+ /// 截断所有字符串属性 以符合[MaxLength(x)]特性
+ ///
+ public void TruncateStrings()
+ {
+ foreach (var property in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.PropertyType == typeof(string))) {
+ var maxLen = property.GetCustomAttribute(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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/EntityBase.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/EntityBase.cs
new file mode 100644
index 00000000..6e517e64
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/EntityBase.cs
@@ -0,0 +1,13 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+/// 数据库实体基类
+///
+public abstract record EntityBase : DataAbstraction
+ where T : IEquatable
+{
+ ///
+ /// 唯一编码
+ ///
+ public virtual T Id { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedClientIp.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedClientIp.cs
new file mode 100644
index 00000000..26739390
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedClientIp.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 创建者客户端IP字段接口
+///
+public interface IFieldCreatedClientIp
+{
+ ///
+ /// 创建者客户端IP
+ ///
+ int? CreatedClientIp { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedClientUserAgent.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedClientUserAgent.cs
new file mode 100644
index 00000000..8c1b2f40
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedClientUserAgent.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 创建者客户端用户代理字段接口
+///
+public interface IFieldCreatedClientUserAgent
+{
+ ///
+ /// 创建者客户端用户代理
+ ///
+ string CreatedUserAgent { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedTime.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedTime.cs
new file mode 100644
index 00000000..e2e2336c
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedTime.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 创建时间字段接口
+///
+public interface IFieldCreatedTime
+{
+ ///
+ /// 创建时间
+ ///
+ DateTime CreatedTime { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedUser.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedUser.cs
new file mode 100644
index 00000000..cd6200d4
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldCreatedUser.cs
@@ -0,0 +1,17 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 创建用户字段接口
+///
+public interface IFieldCreatedUser
+{
+ ///
+ /// 创建者编号
+ ///
+ long? CreatedUserId { get; init; }
+
+ ///
+ /// 创建者用户名
+ ///
+ string CreatedUserName { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldEnabled.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldEnabled.cs
new file mode 100644
index 00000000..a537d5cb
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldEnabled.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 启用字段接口
+///
+public interface IFieldEnabled
+{
+ ///
+ /// 是否启用
+ ///
+ bool Enabled { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedClientIp.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedClientIp.cs
new file mode 100644
index 00000000..a301488d
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedClientIp.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 修改客户端IP字段接口
+///
+public interface IFieldModifiedClientIp
+{
+ ///
+ /// 客户端IP
+ ///
+ int ModifiedClientIp { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedClientUserAgent.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedClientUserAgent.cs
new file mode 100644
index 00000000..74fb59dc
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedClientUserAgent.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 修改客户端用户代理字段接口
+///
+public interface IFieldModifiedClientUserAgent
+{
+ ///
+ /// 客户端用户代理
+ ///
+ string ModifiedUserAgent { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedTime.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedTime.cs
new file mode 100644
index 00000000..50f55804
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedTime.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 修改时间字段接口
+///
+public interface IFieldModifiedTime
+{
+ ///
+ /// 修改时间
+ ///
+ DateTime? ModifiedTime { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedUser.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedUser.cs
new file mode 100644
index 00000000..92df41a6
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldModifiedUser.cs
@@ -0,0 +1,17 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 修改用户字段接口
+///
+public interface IFieldModifiedUser
+{
+ ///
+ /// 修改者编号
+ ///
+ long? ModifiedUserId { get; init; }
+
+ ///
+ /// 修改者用户名
+ ///
+ string ModifiedUserName { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldOwner.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldOwner.cs
new file mode 100644
index 00000000..91b45072
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldOwner.cs
@@ -0,0 +1,17 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 拥有者字段接口
+///
+public interface IFieldOwner
+{
+ ///
+ /// 拥有者部门编号
+ ///
+ long? OwnerDeptId { get; init; }
+
+ ///
+ /// 拥有者用户编号
+ ///
+ long? OwnerId { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldSort.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldSort.cs
new file mode 100644
index 00000000..620cc0fa
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldSort.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 排序字段接口
+///
+public interface IFieldSort
+{
+ ///
+ /// 排序值,越大越前
+ ///
+ long Sort { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldSummary.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldSummary.cs
new file mode 100644
index 00000000..48353a24
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldSummary.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 备注字段接口
+///
+public interface IFieldSummary
+{
+ ///
+ /// 备注
+ ///
+ string Summary { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldVersion.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldVersion.cs
new file mode 100644
index 00000000..56d7d54b
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/Fields/IFieldVersion.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
+
+///
+/// 版本字段接口
+///
+public interface IFieldVersion
+{
+ ///
+ /// 数据版本
+ ///
+ long Version { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/ImmutableEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/ImmutableEntity.cs
new file mode 100644
index 00000000..09d5ed74
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/ImmutableEntity.cs
@@ -0,0 +1,44 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record ImmutableEntity : ImmutableEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 不可变实体
+///
+/// 主键类型
+public abstract record ImmutableEntity : LiteImmutableEntity, IFieldCreatedUser
+ where T : IEquatable
+{
+ ///
+ /// 创建者编号
+ ///
+ [Column(CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public long? CreatedUserId { get; init; }
+
+ ///
+ /// 创建者用户名
+ ///
+ [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual string CreatedUserName { get; init; }
+
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ public override T Id { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteImmutableEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteImmutableEntity.cs
new file mode 100644
index 00000000..7d22adc6
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteImmutableEntity.cs
@@ -0,0 +1,37 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record LiteImmutableEntity : LiteImmutableEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 轻型不可变实体
+///
+/// 主键类型
+public abstract record LiteImmutableEntity : EntityBase, IFieldCreatedTime
+ where T : IEquatable
+{
+ ///
+ /// 创建时间
+ ///
+ [Column(ServerTime = DateTimeKind.Local, CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual DateTime CreatedTime { get; init; }
+
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public override T Id { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteMutableEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteMutableEntity.cs
new file mode 100644
index 00000000..06824910
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteMutableEntity.cs
@@ -0,0 +1,35 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record LiteMutableEntity : LiteMutableEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 轻型可变实体
+///
+public abstract record LiteMutableEntity : LiteImmutableEntity, IFieldModifiedTime
+ where T : IEquatable
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ public override T Id { get; init; }
+
+ ///
+ /// 修改时间
+ ///
+ [Column(ServerTime = DateTimeKind.Local, CanInsert = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual DateTime? ModifiedTime { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteVersionEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteVersionEntity.cs
new file mode 100644
index 00000000..53e7f209
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/LiteVersionEntity.cs
@@ -0,0 +1,36 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record LiteVersionEntity : LiteVersionEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 乐观锁轻型可变实体
+///
+public abstract record LiteVersionEntity : LiteMutableEntity, IFieldVersion
+ where T : IEquatable
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override T Id { get; init; }
+
+ ///
+ /// 数据版本
+ ///
+ [Column(IsVersion = true, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual long Version { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/MutableEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/MutableEntity.cs
new file mode 100644
index 00000000..81120cc3
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/MutableEntity.cs
@@ -0,0 +1,59 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record MutableEntity : MutableEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 可变实体
+///
+public abstract record MutableEntity : LiteMutableEntity, IFieldCreatedUser, IFieldModifiedUser
+ where T : IEquatable
+{
+ ///
+ /// 创建者编号
+ ///
+ [Column(CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual long? CreatedUserId { get; init; }
+
+ ///
+ /// 创建者用户名
+ ///
+ [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual string CreatedUserName { get; init; }
+
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ public override T Id { get; init; }
+
+ ///
+ /// 修改者编号
+ ///
+ [Column(CanInsert = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public long? ModifiedUserId { get; init; }
+
+ ///
+ /// 修改者用户名
+ ///
+ [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public string ModifiedUserName { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/SimpleEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/SimpleEntity.cs
new file mode 100644
index 00000000..12698287
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/SimpleEntity.cs
@@ -0,0 +1,19 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record SimpleEntity : SimpleEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 简单实体
+///
+public abstract record SimpleEntity : EntityBase
+ where T : IEquatable;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/VersionEntity.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/VersionEntity.cs
new file mode 100644
index 00000000..939b8093
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Dependency/VersionEntity.cs
@@ -0,0 +1,59 @@
+namespace NetAdmin.Domain.DbMaps.Dependency;
+
+///
+public abstract record VersionEntity : VersionEntity
+{
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ [Snowflake]
+ public override long Id { get; init; }
+}
+
+///
+/// 乐观锁可变实体
+///
+public abstract record VersionEntity : LiteVersionEntity, IFieldModifiedUser, IFieldCreatedUser
+ where T : IEquatable
+{
+ ///
+ /// 创建者编号
+ ///
+ [Column(CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual long? CreatedUserId { get; init; }
+
+ ///
+ /// 创建者用户名
+ ///
+ [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual string CreatedUserName { get; init; }
+
+ ///
+ /// 唯一编码
+ ///
+ [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
+ [CsvIgnore]
+ public override T Id { get; init; }
+
+ ///
+ /// 修改者编号
+ ///
+ [Column(CanInsert = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual long? ModifiedUserId { get; init; }
+
+ ///
+ /// 修改者用户名
+ ///
+ [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)]
+ [CsvIgnore]
+ [JsonIgnore]
+ public virtual string ModifiedUserName { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Tpl/Tpl_Example.cs b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Tpl/Tpl_Example.cs
new file mode 100644
index 00000000..1bcbaff2
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/DbMaps/Tpl/Tpl_Example.cs
@@ -0,0 +1,7 @@
+namespace NetAdmin.Domain.DbMaps.Tpl;
+
+///
+/// 示例表
+///
+[Table(Name = Chars.FLG_DB_TABLE_NAME_PREFIX + nameof(Tpl_Example))]
+public record Tpl_Example : VersionEntity;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/BulkReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/BulkReq.cs
new file mode 100644
index 00000000..db5d48a7
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/BulkReq.cs
@@ -0,0 +1,16 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+/// 批量请求
+///
+public sealed record BulkReq : DataAbstraction
+ where T : DataAbstraction, new()
+{
+ ///
+ /// 请求对象
+ ///
+ [MaxLength(Numbers.MAX_LIMIT_BULK_REQ)]
+ [MinLength(1)]
+ [Required(ErrorMessageResourceType = typeof(Ln), ErrorMessageResourceName = nameof(Ln.请求对象不能为空))]
+ public IEnumerable Items { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/DelReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/DelReq.cs
new file mode 100644
index 00000000..c1638384
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/DelReq.cs
@@ -0,0 +1,14 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+public sealed record DelReq : DelReq;
+
+///
+/// 请求:通过编号删除
+///
+public record DelReq : EntityBase
+ where T : IEquatable
+{
+ ///
+ public override T Id { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/IPagedInfo.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/IPagedInfo.cs
new file mode 100644
index 00000000..c02bb3fe
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/IPagedInfo.cs
@@ -0,0 +1,17 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+/// 信息:分页
+///
+public interface IPagedInfo
+{
+ ///
+ /// 当前页码
+ ///
+ int Page { get; init; }
+
+ ///
+ /// 页容量
+ ///
+ int PageSize { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/NopReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/NopReq.cs
new file mode 100644
index 00000000..667c3045
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/NopReq.cs
@@ -0,0 +1,6 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+/// 空请求
+///
+public sealed record NopReq : DataAbstraction;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/PagedQueryReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/PagedQueryReq.cs
new file mode 100644
index 00000000..71e55d33
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/PagedQueryReq.cs
@@ -0,0 +1,16 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+/// 请求:分页查询
+///
+public sealed record PagedQueryReq : QueryReq, IPagedInfo
+ where T : DataAbstraction, new()
+{
+ ///
+ [Range(1, Numbers.MAX_LIMIT_QUERY_PAGE_NO)]
+ public int Page { get; init; } = 1;
+
+ ///
+ [Range(1, Numbers.MAX_LIMIT_QUERY_PAGE_SIZE)]
+ public int PageSize { get; init; } = Numbers.DEF_PAGE_SIZE_QUERY;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/PagedQueryRsp.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/PagedQueryRsp.cs
new file mode 100644
index 00000000..b94aeff0
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/PagedQueryRsp.cs
@@ -0,0 +1,24 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+/// 响应:分页查询
+///
+public sealed record PagedQueryRsp(int Page, int PageSize, long Total, IEnumerable Rows) : IPagedInfo
+ where T : DataAbstraction
+{
+ ///
+ /// 数据行
+ ///
+ public IEnumerable Rows { get; } = Rows;
+
+ ///
+ public int Page { get; init; } = Page;
+
+ ///
+ public int PageSize { get; init; } = PageSize;
+
+ ///
+ /// 数据总条
+ ///
+ public long Total { get; init; } = Total;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/QueryReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/QueryReq.cs
new file mode 100644
index 00000000..3b8ff65d
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Dependency/QueryReq.cs
@@ -0,0 +1,72 @@
+namespace NetAdmin.Domain.Dto.Dependency;
+
+///
+/// 请求:查询
+///
+public record QueryReq : DataAbstraction
+ where T : DataAbstraction, new()
+{
+ ///
+ /// 取前n条
+ ///
+ [Range(1, Numbers.MAX_LIMIT_QUERY)]
+ public int Count { get; init; } = Numbers.MAX_LIMIT_QUERY;
+
+ ///
+ /// 动态查询条件
+ ///
+ public DynamicFilterInfo DynamicFilter { get; init; }
+
+ ///
+ /// 查询条件
+ ///
+ public T Filter { get; init; }
+
+ ///
+ /// 查询关键字
+ ///
+ public string Keywords { get; init; }
+
+ ///
+ /// 排序方式
+ ///
+ public Orders? Order { get; init; }
+
+ ///
+ /// 排序字段
+ ///
+ public string Prop { get; init; }
+
+ ///
+ /// 所需字段
+ ///
+ public string[] RequiredFields { get; set; }
+
+ ///
+ /// 列表表达式
+ ///
+ public Expression> GetToListExp()
+ {
+ if (RequiredFields.NullOrEmpty()) {
+ return null;
+ }
+
+ var expParameter = Expression.Parameter(typeof(TEntity), "a");
+ var bindings = new List();
+
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ foreach (var field in RequiredFields) {
+ var prop = typeof(TEntity).GetProperty(field);
+ if (prop == null || prop.GetCustomAttribute() != 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>(expBody, expParameter);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/DfBuilder.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/DfBuilder.cs
new file mode 100644
index 00000000..087d8297
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/DfBuilder.cs
@@ -0,0 +1,17 @@
+using NetAdmin.Domain.Enums;
+
+namespace NetAdmin.Domain.Dto;
+
+///
+/// 动态过滤条件生成器
+///
+public sealed record DfBuilder
+{
+ ///
+ /// 构建生成器
+ ///
+ public static DynamicFilterInfo New(DynamicFilterLogics logic)
+ {
+ return new DynamicFilterInfo { Logic = logic, Filters = [] };
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/DynamicFilterInfo.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/DynamicFilterInfo.cs
new file mode 100644
index 00000000..4742cdbb
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/DynamicFilterInfo.cs
@@ -0,0 +1,125 @@
+using NetAdmin.Domain.Enums;
+
+namespace NetAdmin.Domain.Dto;
+
+///
+/// 动态过滤条件
+///
+public sealed record DynamicFilterInfo : DataAbstraction
+{
+ ///
+ /// 字段名
+ ///
+ public string Field { get; init; }
+
+ ///
+ /// 子过滤条件
+ ///
+ public List Filters { get; init; }
+
+ ///
+ /// 子过滤条件逻辑关系
+ ///
+ public DynamicFilterLogics Logic { get; init; }
+
+ ///
+ /// 操作符
+ ///
+ public DynamicFilterOperators Operator { get; init; }
+
+ ///
+ /// 值
+ ///
+ public object Value { get; init; }
+
+ ///
+ /// 隐式转换为 FreeSql 的 DynamicFilterInfo 对象
+ ///
+ public static implicit operator FreeSql.Internal.Model.DynamicFilterInfo(DynamicFilterInfo d)
+ {
+ var ret = d.Adapt();
+ ProcessDynamicFilter(ret);
+ return ret;
+ }
+
+ ///
+ /// 添加子过滤条件
+ ///
+ public DynamicFilterInfo Add(DynamicFilterInfo df)
+ {
+ if (Filters == null) {
+ return this with { Filters = [df] };
+ }
+
+ Filters.Add(df);
+ return this;
+ }
+
+ ///
+ /// 添加过滤条件
+ ///
+ public DynamicFilterInfo Add(string field, DynamicFilterOperators opt, object val)
+ {
+ return Add(new DynamicFilterInfo { Field = field, Operator = opt, Value = val });
+ }
+
+ ///
+ /// 添加过滤条件
+ ///
+ public DynamicFilterInfo AddIf(bool condition, string field, DynamicFilterOperators opt, object val)
+ {
+ return !condition ? this : Add(field, opt, val);
+ }
+
+ ///
+ /// 添加过滤条件
+ ///
+ 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();
+ if (!DateTime.TryParse(values[0], CultureInfo.InvariantCulture, out _)) {
+ var result = values[0]
+ .ExecuteCSharpCodeAsync([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([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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/RestfulInfo.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/RestfulInfo.cs
new file mode 100644
index 00000000..3286b5e6
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/RestfulInfo.cs
@@ -0,0 +1,25 @@
+namespace NetAdmin.Domain.Dto;
+
+///
+/// 信息:RESTful 风格结果集
+///
+public record RestfulInfo : DataAbstraction
+{
+ ///
+ /// 代码
+ ///
+ /// succeed
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public ErrorCodes Code { get; init; }
+
+ ///
+ /// 数据
+ ///
+ public T Data { get; init; }
+
+ ///
+ /// 字符串:"消息内容",或数组:[{"参数名1":"消息内容1"},{"参数名2":"消息内容2"}]
+ ///
+ /// 请求成功
+ public object Msg { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/CreateExampleReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/CreateExampleReq.cs
new file mode 100644
index 00000000..bf6eeb95
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/CreateExampleReq.cs
@@ -0,0 +1,8 @@
+using NetAdmin.Domain.DbMaps.Tpl;
+
+namespace NetAdmin.Domain.Dto.Tpl.Example;
+
+///
+/// 请求:创建示例
+///
+public record CreateExampleReq : Tpl_Example;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/QueryExampleReq.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/QueryExampleReq.cs
new file mode 100644
index 00000000..dbeb8849
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/QueryExampleReq.cs
@@ -0,0 +1,13 @@
+using NetAdmin.Domain.DbMaps.Tpl;
+
+namespace NetAdmin.Domain.Dto.Tpl.Example;
+
+///
+/// 请求:查询示例
+///
+public sealed record QueryExampleReq : Tpl_Example
+{
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public override long Id { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/QueryExampleRsp.cs b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/QueryExampleRsp.cs
new file mode 100644
index 00000000..27e7dffe
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Dto/Tpl/Example/QueryExampleRsp.cs
@@ -0,0 +1,17 @@
+using NetAdmin.Domain.DbMaps.Tpl;
+
+namespace NetAdmin.Domain.Dto.Tpl.Example;
+
+///
+/// 响应:查询示例
+///
+public sealed record QueryExampleRsp : Tpl_Example
+{
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public override long Id { get; init; }
+
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public override long Version { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Enums/DynamicFilterLogics.cs b/src/backend/NetAdmin/NetAdmin.Domain/Enums/DynamicFilterLogics.cs
new file mode 100644
index 00000000..13c768eb
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Enums/DynamicFilterLogics.cs
@@ -0,0 +1,22 @@
+namespace NetAdmin.Domain.Enums;
+
+///
+/// 动态查询条件逻辑运算符
+///
+[Export]
+public enum DynamicFilterLogics
+{
+ ///
+ /// 并且
+ ///
+ [ResourceDescription(nameof(Ln.并且))]
+ And = 0
+
+ ,
+
+ ///
+ /// 或者
+ ///
+ [ResourceDescription(nameof(Ln.或者))]
+ Or = 1
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Enums/DynamicFilterOperators.cs b/src/backend/NetAdmin/NetAdmin.Domain/Enums/DynamicFilterOperators.cs
new file mode 100644
index 00000000..dce8162b
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Enums/DynamicFilterOperators.cs
@@ -0,0 +1,158 @@
+namespace NetAdmin.Domain.Enums;
+
+///
+/// 动态查询条件操作符
+///
+[Export]
+public enum DynamicFilterOperators
+{
+ ///
+ /// 包含
+ ///
+ [ResourceDescription(nameof(Ln.包含))]
+ Contains = 0
+
+ ,
+
+ ///
+ /// 以什么开始
+ ///
+ [ResourceDescription(nameof(Ln.以什么开始))]
+ StartsWith = 1
+
+ ,
+
+ ///
+ /// 以什么结束
+ ///
+ [ResourceDescription(nameof(Ln.以什么结束))]
+ EndsWith = 2
+
+ ,
+
+ ///
+ /// 不包含
+ ///
+ [ResourceDescription(nameof(Ln.不包含))]
+ NotContains = 3
+
+ ,
+
+ ///
+ /// 不以什么开始
+ ///
+ [ResourceDescription(nameof(Ln.不以什么开始))]
+ NotStartsWith = 4
+
+ ,
+
+ ///
+ /// 不以什么结束
+ ///
+ [ResourceDescription(nameof(Ln.不以什么结束))]
+ NotEndsWith = 5
+
+ ,
+
+ ///
+ /// 等于
+ ///
+ [ResourceDescription(nameof(Ln.等于))]
+ Equal = 6
+
+ ,
+
+ ///
+ /// 等于
+ ///
+ [ResourceDescription(nameof(Ln.等于))]
+ Equals = 7
+
+ ,
+
+ ///
+ /// 等于
+ ///
+ [ResourceDescription(nameof(Ln.等于))]
+ Eq = 8
+
+ ,
+
+ ///
+ /// 不等于
+ ///
+ [ResourceDescription(nameof(Ln.不等于))]
+ NotEqual = 9
+
+ ,
+
+ ///
+ /// 大于
+ ///
+ [ResourceDescription(nameof(Ln.大于))]
+ GreaterThan = 10
+
+ ,
+
+ ///
+ /// 大于等于
+ ///
+ [ResourceDescription(nameof(Ln.大于等于))]
+ GreaterThanOrEqual = 11
+
+ ,
+
+ ///
+ /// 小于
+ ///
+ [ResourceDescription(nameof(Ln.小于))]
+ LessThan = 12
+
+ ,
+
+ ///
+ /// 小于等于
+ ///
+ [ResourceDescription(nameof(Ln.小于等于))]
+ LessThanOrEqual = 13
+
+ ,
+
+ ///
+ /// 范围
+ ///
+ [ResourceDescription(nameof(Ln.范围))]
+ Range = 14
+
+ ,
+
+ ///
+ /// 日期范围
+ ///
+ [ResourceDescription(nameof(Ln.日期范围))]
+ DateRange = 15
+
+ ,
+
+ ///
+ /// 为其中之一
+ ///
+ [ResourceDescription(nameof(Ln.为其中之一))]
+ Any = 16
+
+ ,
+
+ ///
+ /// 不为其中之一
+ ///
+ [ResourceDescription(nameof(Ln.不为其中之一))]
+ NotAny = 17
+
+ ,
+
+ ///
+ /// 自定义
+ ///
+ [ResourceDescription(nameof(Ln.自定义))]
+ Custom = 18
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Enums/HttpMethods.cs b/src/backend/NetAdmin/NetAdmin.Domain/Enums/HttpMethods.cs
new file mode 100644
index 00000000..972bc766
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Enums/HttpMethods.cs
@@ -0,0 +1,69 @@
+namespace NetAdmin.Domain.Enums;
+
+///
+/// HTTP 请求方法
+///
+[Export]
+public enum HttpMethods
+{
+ ///
+ /// Connect
+ ///
+ Connect = 1
+
+ ,
+
+ ///
+ /// Delete
+ ///
+ Delete = 2
+
+ ,
+
+ ///
+ /// Get
+ ///
+ Get = 3
+
+ ,
+
+ ///
+ /// Head
+ ///
+ Head = 4
+
+ ,
+
+ ///
+ /// Options
+ ///
+ Options = 5
+
+ ,
+
+ ///
+ /// Patch
+ ///
+ Patch = 6
+
+ ,
+
+ ///
+ /// Post
+ ///
+ Post = 7
+
+ ,
+
+ ///
+ /// Put
+ ///
+ Put = 8
+
+ ,
+
+ ///
+ /// Trace
+ ///
+ Trace = 9
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/IEventSourceGeneric.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/IEventSourceGeneric.cs
new file mode 100644
index 00000000..b784b80b
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/IEventSourceGeneric.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// 泛型事件源接口
+///
+public interface IEventSourceGeneric : IEventSource
+{
+ ///
+ /// 事件承载(携带)数据
+ ///
+ T Data { get; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/SeedDataInsertedEvent.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/SeedDataInsertedEvent.cs
new file mode 100644
index 00000000..fe9ea3e2
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/SeedDataInsertedEvent.cs
@@ -0,0 +1,38 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// 种子数据插入完毕事件
+///
+public sealed record SeedDataInsertedEvent : DataAbstraction, IEventSource
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SeedDataInsertedEvent(int insertedCount, bool isConsumOnce = false)
+ {
+ IsConsumOnce = isConsumOnce;
+ InsertedCount = insertedCount;
+ CreatedTime = DateTime.Now;
+ EventId = nameof(SeedDataInsertedEvent);
+ }
+
+ ///
+ public DateTime CreatedTime { get; }
+
+ ///
+ public string EventId { get; }
+
+ ///
+ public bool IsConsumOnce { get; }
+
+ ///
+ public CancellationToken CancellationToken { get; init; }
+
+ ///
+ /// 插入数量
+ ///
+ public int InsertedCount { get; set; }
+
+ ///
+ public object Payload { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandAfterEvent.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandAfterEvent.cs
new file mode 100644
index 00000000..a320670f
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandAfterEvent.cs
@@ -0,0 +1,29 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// Sql命令执行后事件
+///
+public sealed record SqlCommandAfterEvent : SqlCommandBeforeEvent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SqlCommandAfterEvent(CommandAfterEventArgs e) //
+ : base(e)
+ {
+ ElapsedMilliseconds = (long)((double)e.ElapsedTicks / Stopwatch.Frequency * 1_000);
+ EventId = nameof(SqlCommandAfterEvent);
+ }
+
+ ///
+ /// 耗时(单位:毫秒)
+ ///
+ /// de
+ private long ElapsedMilliseconds { get; }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "SQL-{0}: {2} ms {1}", Id, Sql, ElapsedMilliseconds);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandBeforeEvent.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandBeforeEvent.cs
new file mode 100644
index 00000000..5a96632a
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandBeforeEvent.cs
@@ -0,0 +1,24 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// Sql命令执行前事件
+///
+public record SqlCommandBeforeEvent : SqlCommandEvent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SqlCommandBeforeEvent(CommandBeforeEventArgs e)
+ {
+ Identifier = e.Identifier;
+ Sql = e.Command.ParameterFormat().RemoveWrapped();
+ EventId = nameof(SqlCommandBeforeEvent);
+ CreatedTime = DateTime.Now;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "SQL-{0}: Executing...", Id);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandEvent.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandEvent.cs
new file mode 100644
index 00000000..88a877a7
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/SqlCommandEvent.cs
@@ -0,0 +1,45 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// Sql命令事件
+///
+public abstract record SqlCommandEvent : DataAbstraction, IEventSource
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected SqlCommandEvent(bool isConsumOnce = false)
+ {
+ IsConsumOnce = isConsumOnce;
+ }
+
+ ///
+ public bool IsConsumOnce { get; }
+
+ ///
+ public CancellationToken CancellationToken { get; init; }
+
+ ///
+ public DateTime CreatedTime { get; protected init; }
+
+ ///
+ public string EventId { get; protected init; }
+
+ ///
+ public object Payload { get; init; }
+
+ ///
+ /// 标识符缩写
+ ///
+ protected string Id => Identifier.ToString()[..8].ToUpperInvariant();
+
+ ///
+ /// 标识符,可将 CommandBefore 与 CommandAfter 进行匹配
+ ///
+ protected Guid Identifier { get; init; }
+
+ ///
+ /// 关联的Sql语句
+ ///
+ protected string Sql { get; init; }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/SyncStructureAfterEvent.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/SyncStructureAfterEvent.cs
new file mode 100644
index 00000000..2bdee6ac
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/SyncStructureAfterEvent.cs
@@ -0,0 +1,22 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// 同步数据库结构之后事件
+///
+public sealed record SyncStructureAfterEvent : SyncStructureBeforeEvent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SyncStructureAfterEvent(SyncStructureBeforeEventArgs e) //
+ : base(e)
+ {
+ EventId = nameof(SyncStructureAfterEvent);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}: {1}: {2}", Id, Ln.数据库结构同步完成, string.Join(',', EntityTypes.Select(x => x.Name)));
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/Events/SyncStructureBeforeEvent.cs b/src/backend/NetAdmin/NetAdmin.Domain/Events/SyncStructureBeforeEvent.cs
new file mode 100644
index 00000000..a1327036
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/Events/SyncStructureBeforeEvent.cs
@@ -0,0 +1,29 @@
+namespace NetAdmin.Domain.Events;
+
+///
+/// 同步数据库结构之前事件
+///
+public record SyncStructureBeforeEvent : SqlCommandEvent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SyncStructureBeforeEvent(SyncStructureBeforeEventArgs e)
+ {
+ Identifier = e.Identifier;
+ EventId = nameof(SyncStructureBeforeEvent);
+ CreatedTime = DateTime.Now;
+ EntityTypes = e.EntityTypes;
+ }
+
+ ///
+ /// 实体类型
+ ///
+ protected Type[] EntityTypes { get; }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}: {1}", Id, Ln.数据库同步开始);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/NetAdmin.Domain.csproj b/src/backend/NetAdmin/NetAdmin.Domain/NetAdmin.Domain.csproj
new file mode 100644
index 00000000..681c72f9
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/NetAdmin.Domain.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Domain/ProjectUsings.cs b/src/backend/NetAdmin/NetAdmin.Domain/ProjectUsings.cs
new file mode 100644
index 00000000..4f775145
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Domain/ProjectUsings.cs
@@ -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;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Attributes/RemoveNullNodeAttribute.cs b/src/backend/NetAdmin/NetAdmin.Host/Attributes/RemoveNullNodeAttribute.cs
new file mode 100644
index 00000000..1cc787f9
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Attributes/RemoveNullNodeAttribute.cs
@@ -0,0 +1,7 @@
+namespace NetAdmin.Host.Attributes;
+
+///
+/// 标记一个Action,其响应的json结果会被删除值为null的节点
+///
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class RemoveNullNodeAttribute : Attribute;
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Attributes/TransactionAttribute.cs b/src/backend/NetAdmin/NetAdmin.Host/Attributes/TransactionAttribute.cs
new file mode 100644
index 00000000..2c74403d
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Attributes/TransactionAttribute.cs
@@ -0,0 +1,41 @@
+namespace NetAdmin.Host.Attributes;
+
+///
+/// 标记一个Action启用事务
+///
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class TransactionAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TransactionAttribute() { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TransactionAttribute(Propagation propagation) //
+ : this(null, propagation) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TransactionAttribute(IsolationLevel isolationLevel, Propagation propagation) //
+ : this(new IsolationLevel?(isolationLevel), propagation) { }
+
+ private TransactionAttribute(IsolationLevel? isolationLevel, Propagation propagation)
+ {
+ IsolationLevel = isolationLevel;
+ Propagation = propagation;
+ }
+
+ ///
+ /// 事务隔离级别
+ ///
+ public IsolationLevel? IsolationLevel { get; }
+
+ ///
+ /// 事务传播方式
+ ///
+ public Propagation Propagation { get; } = Propagation.Required;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/IPollingWork.cs b/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/IPollingWork.cs
new file mode 100644
index 00000000..c3a73437
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/IPollingWork.cs
@@ -0,0 +1,12 @@
+namespace NetAdmin.Host.BackgroundRunning;
+
+///
+/// 轮询工作接口
+///
+public interface IPollingWork
+{
+ ///
+ /// 启动工作
+ ///
+ ValueTask StartAsync(CancellationToken cancelToken);
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/PollingWork.cs b/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/PollingWork.cs
new file mode 100644
index 00000000..9b0f68c3
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/PollingWork.cs
@@ -0,0 +1,15 @@
+using NetAdmin.Domain;
+
+namespace NetAdmin.Host.BackgroundRunning;
+
+///
+/// 轮询工作
+///
+public abstract class PollingWork(TWorkData workData) : WorkBase
+ where TWorkData : DataAbstraction
+{
+ ///
+ /// 工作数据
+ ///
+ protected TWorkData WorkData => workData;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/WorkBase.cs b/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/WorkBase.cs
new file mode 100644
index 00000000..0400fddb
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/BackgroundRunning/WorkBase.cs
@@ -0,0 +1,75 @@
+using StackExchange.Redis;
+
+namespace NetAdmin.Host.BackgroundRunning;
+
+///
+/// 工作基类
+///
+public abstract class WorkBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected WorkBase()
+ {
+ ServiceProvider = App.GetService().CreateScope().ServiceProvider;
+ UowManager = ServiceProvider.GetService();
+ Logger = ServiceProvider.GetService>();
+ }
+
+ ///
+ /// 日志记录器
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ /// 服务提供器
+ ///
+ protected IServiceProvider ServiceProvider { get; }
+
+ ///
+ /// 事务单元管理器
+ ///
+ protected UnitOfWorkManager UowManager { get; }
+
+ ///
+ /// 通用工作流
+ ///
+ protected abstract ValueTask WorkflowAsync( //
+
+ // ReSharper disable once UnusedParameter.Global
+ #pragma warning disable SA1114
+ CancellationToken cancelToken);
+ #pragma warning restore SA1114
+
+ ///
+ /// 通用工作流
+ ///
+ /// 加锁失败异常
+ 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);
+ }
+
+ ///
+ /// 获取锁
+ ///
+ private Task GetLockerAsync(string lockId)
+ {
+ var db = ServiceProvider.GetService()
+ .GetDatabase(ServiceProvider.GetService>()
+ .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));
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Controllers/ControllerBase.cs b/src/backend/NetAdmin/NetAdmin.Host/Controllers/ControllerBase.cs
new file mode 100644
index 00000000..fe7bce0e
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Controllers/ControllerBase.cs
@@ -0,0 +1,17 @@
+using NetAdmin.Application.Services;
+using NetAdmin.Cache;
+
+namespace NetAdmin.Host.Controllers;
+
+///
+/// 控制器基类
+///
+public abstract class ControllerBase(TCache cache = default) : IDynamicApiController
+ where TCache : ICache //
+ where TService : IService
+{
+ ///
+ /// 关联的缓存
+ ///
+ protected TCache Cache => cache;
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Controllers/ProbeController.cs b/src/backend/NetAdmin/NetAdmin.Host/Controllers/ProbeController.cs
new file mode 100644
index 00000000..29d2f174
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Controllers/ProbeController.cs
@@ -0,0 +1,98 @@
+using NetAdmin.Application.Services;
+using NetAdmin.Cache;
+using NetAdmin.Host.Middlewares;
+
+namespace NetAdmin.Host.Controllers;
+
+///
+/// 探针组件
+///
+[ApiDescriptionSettings("Probe")]
+[Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)]
+public sealed class ProbeController : ControllerBase, IService>
+{
+ ///
+ /// 退出程序
+ ///
+ [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();
+ }
+
+ ///
+ /// 健康检查
+ ///
+ [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
+ };
+ }
+
+ ///
+ /// 系统是否已经安全停止
+ ///
+ [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" };
+ }
+
+ ///
+ /// 实例下线
+ ///
+ ///
+ /// 流量只出不进
+ ///
+ [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();
+ }
+
+ ///
+ /// 停止日志计数器
+ ///
+ [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();
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Controllers/Tpl/ExampleController.cs b/src/backend/NetAdmin/NetAdmin.Host/Controllers/Tpl/ExampleController.cs
new file mode 100644
index 00000000..29698b7b
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Controllers/Tpl/ExampleController.cs
@@ -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;
+
+///
+/// 示例服务
+///
+[ApiDescriptionSettings(nameof(Tpl), Module = nameof(Tpl))]
+[Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)]
+public sealed class ExampleController(IExampleCache cache) : ControllerBase(cache), IExampleModule
+{
+ ///
+ /// 批量删除示例
+ ///
+ [Transaction]
+ public Task BulkDeleteAsync(BulkReq req)
+ {
+ return Cache.BulkDeleteAsync(req);
+ }
+
+ ///
+ /// 示例计数
+ ///
+ public Task CountAsync(QueryReq req)
+ {
+ return Cache.CountAsync(req);
+ }
+
+ ///
+ /// 创建示例
+ ///
+ [Transaction]
+ public Task CreateAsync(CreateExampleReq req)
+ {
+ return Cache.CreateAsync(req);
+ }
+
+ ///
+ /// 删除示例
+ ///
+ [Transaction]
+ public Task DeleteAsync(DelReq req)
+ {
+ return Cache.DeleteAsync(req);
+ }
+
+ ///
+ /// 示例是否存在
+ ///
+ [NonAction]
+ public Task ExistAsync(QueryReq req)
+ {
+ return Cache.ExistAsync(req);
+ }
+
+ ///
+ /// 导出示例
+ ///
+ [NonAction]
+ public Task ExportAsync(QueryReq req)
+ {
+ return Cache.ExportAsync(req);
+ }
+
+ ///
+ /// 获取单个示例
+ ///
+ public Task GetAsync(QueryExampleReq req)
+ {
+ return Cache.GetAsync(req);
+ }
+
+ ///
+ /// 分页查询示例
+ ///
+ public Task> PagedQueryAsync(PagedQueryReq req)
+ {
+ return Cache.PagedQueryAsync(req);
+ }
+
+ ///
+ /// 查询示例
+ ///
+ [NonAction]
+ public Task> QueryAsync(QueryReq req)
+ {
+ return Cache.QueryAsync(req);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Extensions/HttpContextExtensions.cs b/src/backend/NetAdmin/NetAdmin.Host/Extensions/HttpContextExtensions.cs
new file mode 100644
index 00000000..1cff45e1
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,34 @@
+namespace NetAdmin.Host.Extensions;
+
+///
+/// HttpContext 扩展方法
+///
+public static class HttpContextExtensions
+{
+ private static readonly Regex _nullRegex = new("\"[^\"]+?\":null,?", RegexOptions.Compiled);
+
+ ///
+ /// 删除 response json body 中value 为null的节点
+ ///
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs b/src/backend/NetAdmin/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs
new file mode 100644
index 00000000..d1c9d7bb
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs
@@ -0,0 +1,62 @@
+#if DEBUG
+using IGeekFan.AspNetCore.Knife4jUI;
+
+#else
+using Prometheus;
+using Prometheus.HttpMetrics;
+#endif
+
+namespace NetAdmin.Host.Extensions;
+
+///
+/// ApplicationBuilder对象 扩展方法
+///
+[SuppressSniffer]
+
+// ReSharper disable once InconsistentNaming
+public static class IApplicationBuilderExtensions
+{
+ ///
+ /// 执行匹配的端点
+ ///
+ public static IApplicationBuilder UseEndpoints(this IApplicationBuilder me)
+ {
+ return me.UseEndpoints(endpoints => {
+ _ = endpoints.MapControllers();
+ #if !DEBUG
+ _ = endpoints.MapMetrics();
+ #endif
+ });
+ }
+ #if DEBUG
+ ///
+ /// 使用 api skin (knife4j-vue)
+ ///
+ 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
+ ///
+ /// 使用 Prometheus
+ ///
+ 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
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Extensions/IMvcBuilderExtensions.cs b/src/backend/NetAdmin/NetAdmin.Host/Extensions/IMvcBuilderExtensions.cs
new file mode 100644
index 00000000..e1b6a19a
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Extensions/IMvcBuilderExtensions.cs
@@ -0,0 +1,115 @@
+using NetAdmin.Host.Filters;
+using NetAdmin.Host.Utils;
+
+namespace NetAdmin.Host.Extensions;
+
+///
+/// IMvcBuilder 扩展方法
+///
+[SuppressSniffer]
+
+// ReSharper disable once InconsistentNaming
+public static class IMvcBuilderExtensions
+{
+ ///
+ /// api结果处理器
+ ///
+ public static IMvcBuilder AddDefaultApiResultHandler(this IMvcBuilder me)
+ {
+ return me.AddInjectWithUnifyResult(injectOptions => {
+ injectOptions.ConfigureSwaggerGen(genOptions => {
+ // 替换自定义的EnumSchemaFilter,支持多语言Resx资源 (需将SpecificationDocumentSettings.EnableEnumSchemaFilter配置为false)
+ genOptions.SchemaFilter();
+
+ // 枚举显示自身xml comment 而不是$ref原型引用
+ genOptions.UseInlineDefinitionsForEnums();
+
+ // 将程序集版本号与OpenApi版本号同步
+ foreach (var doc in genOptions.SwaggerGeneratorOptions.SwaggerDocs) {
+ doc.Value.Version = FileVersionInfo
+ .GetVersionInfo(Assembly.GetEntryAssembly()!.Location)
+ .ProductVersion;
+ }
+ });
+ });
+ }
+
+ ///
+ /// Json序列化配置
+ ///
+ ///
+ /// 正反序列化规则:
+ /// object->json:
+ /// 1、值为 null 或 default 的节点将被忽略
+ /// 2、值为 "" 的节点将被忽略。
+ /// 3、值为 [] 的节点将被忽略。
+ /// 4、节点名:大驼峰转小驼峰
+ /// 5、不转义除对json结构具有破坏性(如")以外的任何字符
+ /// 6、大数字原样输出(不加引号),由前端处理js大数兼容问题
+ /// json->object:
+ /// 1、允许带注释的json(自动忽略)
+ /// 2、允许尾随逗号
+ /// 3、节点名大小写不敏感
+ /// 4、允许带双引号的数字
+ /// 5、值为"" 转 null
+ /// 6、值为[] 转 null
+ ///
+ public static IMvcBuilder AddJsonSerializer(this IMvcBuilder me, bool enumToString = false)
+ {
+ return me.AddJsonOptions(options => SetJsonOptions(enumToString, options));
+ }
+
+ ///
+ /// 设置Json选项
+ ///
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Extensions/MethodInfoExtensions.cs b/src/backend/NetAdmin/NetAdmin.Host/Extensions/MethodInfoExtensions.cs
new file mode 100644
index 00000000..e99d91c9
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Extensions/MethodInfoExtensions.cs
@@ -0,0 +1,18 @@
+namespace NetAdmin.Host.Extensions;
+
+///
+/// Type 扩展方法
+///
+public static class MethodInfoExtensions
+{
+ ///
+ /// 获取路由路径
+ ///
+ public static string GetRoutePath(this MethodInfo me, IServiceProvider serviceProvider)
+ {
+ return serviceProvider.GetService()
+ .ActionDescriptors.Items.FirstOrDefault(x => x.DisplayName!.StartsWith( //
+ $"{me.DeclaringType}.{me.Name}", StringComparison.Ordinal))
+ ?.AttributeRouteInfo?.Template;
+ }
+}
\ No newline at end of file
diff --git a/src/backend/NetAdmin/NetAdmin.Host/Extensions/ResourceExecutingContextExtensions.cs b/src/backend/NetAdmin/NetAdmin.Host/Extensions/ResourceExecutingContextExtensions.cs
new file mode 100644
index 00000000..3d5c1a79
--- /dev/null
+++ b/src/backend/NetAdmin/NetAdmin.Host/Extensions/ResourceExecutingContextExtensions.cs
@@ -0,0 +1,19 @@
+using NetAdmin.Domain.Dto;
+
+namespace NetAdmin.Host.Extensions;
+
+///
+/// ResourceExecutingContextExtensions
+///
+public static class ResourceExecutingContextExtensions
+{
+ ///
+ /// 设置失败结果
+ ///
+ public static void SetFailResult(this ResourceExecutingContext me, ErrorCodes errorCode, string errorMsg = null)
+ where T : RestfulInfo