xf 2 anni fa
parent
commit
57d874e2ae
40 ha cambiato i file con 554 aggiunte e 328 eliminazioni
  1. 1 3
      src/Hotline.Api/Controllers/RoleController.cs
  2. 26 14
      src/Hotline.Api/Controllers/UserController.cs
  3. 17 64
      src/Hotline.Api/StartupExtensions.cs
  4. 126 1
      src/Hotline.Api/StartupHelper.cs
  5. 1 1
      src/Hotline.Application.Contracts/Validators/User/AddUserDtoValidator.cs
  6. 1 1
      src/Hotline.Application.Contracts/Validators/User/UpdateUserDtoValidator.cs
  7. 0 73
      src/Hotline.Application/Identity/IIdentityAppService.cs
  8. 78 0
      src/Hotline.Application/Identity/IdentityAppService.cs
  9. 2 3
      src/Hotline.Application/Mappers/MapperConfigs.cs
  10. 9 0
      src/Hotline.Application/Roles/IRoleAppService.cs
  11. 27 0
      src/Hotline.Application/Roles/RoleAppService.cs
  12. 6 6
      src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionFilterBuilder.cs
  13. 0 14
      src/Hotline.Share/Dtos/Role/QueryRolesPagedDto.cs
  14. 9 0
      src/Hotline.Share/Dtos/Roles/QueryRolesPagedDto.cs
  15. 2 3
      src/Hotline.Share/Dtos/Roles/RoleAuthorityDto.cs
  16. 1 7
      src/Hotline.Share/Dtos/Roles/RoleDto.cs
  17. 0 14
      src/Hotline.Share/Dtos/User/SetUserRolesDto.cs
  18. 0 52
      src/Hotline.Share/Dtos/User/UserDto.cs
  19. 0 6
      src/Hotline.Share/Dtos/User/UserPagedDto.cs
  20. 1 7
      src/Hotline.Share/Dtos/Users/ChangePasswordDto.cs
  21. 8 0
      src/Hotline.Share/Dtos/Users/SetUserRolesDto.cs
  22. 89 0
      src/Hotline.Share/Dtos/Users/UserDto.cs
  23. 6 0
      src/Hotline.Share/Dtos/Users/UserPagedDto.cs
  24. 1 1
      src/Hotline.Share/Dtos/Users/UserStateDto.cs
  25. 1 1
      src/Hotline.Share/Dtos/Users/WorkDto.cs
  26. 14 0
      src/Hotline.Share/Enums/Identity/EAccountStatus.cs
  27. 12 0
      src/Hotline.Share/Requests/BatchRemoveDto.cs
  28. 3 0
      src/Hotline/Identity/Accounts/Account.cs
  29. 13 1
      src/Hotline/Identity/Accounts/AccountDomainService.cs
  30. 1 0
      src/Hotline/Identity/Accounts/IAccountDomainService.cs
  31. 1 1
      src/Hotline/Orders/Order.cs
  32. 21 0
      src/Hotline/SeedData/AccountRoleSeedData.cs
  33. 27 0
      src/Hotline/SeedData/RoleSeedData.cs
  34. 3 40
      src/Hotline/SeedData/SysAccountSeedData.cs
  35. 22 0
      src/Hotline/SeedData/UserSeedData.cs
  36. 1 1
      src/Hotline/Users/IUserDomainService.cs
  37. 8 0
      src/Hotline/Users/User.cs
  38. 1 1
      src/Hotline/Users/UserDomainService.cs
  39. 11 9
      src/XF.Domain.Repository/Entity.cs
  40. 4 4
      src/XF.Domain/Entities/IDataPermission.cs

+ 1 - 3
src/Hotline.Api/Controllers/RoleController.cs

@@ -3,10 +3,8 @@ using Hotline.Permissions;
 using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Hotline.Share.Dtos;
-using Hotline.Share.Dtos.Role;
-using Hotline.Share.Requests;
+using Hotline.Share.Dtos.Roles;
 using MapsterMapper;
-using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using XF.Domain.Exceptions;
 using XF.Utility.UnifyResponse;

+ 26 - 14
src/Hotline.Api/Controllers/UserController.cs

@@ -2,7 +2,7 @@
 using Hotline.CallCenter.Tels;
 using Hotline.Identity.Accounts;
 using Hotline.Permissions;
-using Hotline.Share.Dtos.User;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Users;
 using MapsterMapper;
 using Microsoft.AspNetCore.Mvc;
@@ -10,7 +10,10 @@ using XF.Domain.Authentications;
 using XF.Domain.Exceptions;
 using XF.Utility.AppIdentityModel;
 using Hotline.Share.Dtos;
-using Hotline.Share.Dtos.Role;
+using Hotline.Share.Dtos.Roles;
+using Hotline.Share.Dtos.Users;
+using Hotline.Share.Requests;
+using SqlSugar;
 
 namespace Hotline.Api.Controllers;
 
@@ -90,14 +93,18 @@ public class UserController : BaseController
     [HttpGet("paged")]
     public async Task<PagedDto<UserDto>> QueryPaged([FromQuery] UserPagedDto dto)
     {
-        var (total, items) = await _userRepository.QueryPagedAsync(
-            d => true,
-            d => d.OrderByDescending(x => x.CreationTime),
-            dto.PageIndex,
-            dto.PageSize,
-            false,
-            (!string.IsNullOrEmpty(dto.PhoneNo), d => d.PhoneNo.Contains(dto.PhoneNo!)),
-            (!string.IsNullOrEmpty(dto.DisplayName), d => !string.IsNullOrEmpty(d.Name) && d.Name.Contains(dto.DisplayName!)));
+        var (total, items) = await _userRepository.Queryable()
+             .Includes(d => d.Account, x => x.Roles)
+             .Includes(d => d.Organization)
+             .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
+                 d => d.Name.Contains(dto.Keyword!) || d.PhoneNo.Contains(dto.Keyword!))
+             .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => d.OrgCode == dto.OrgCode)
+             .WhereIF(!string.IsNullOrEmpty(dto.Role), d => d.Account.Roles.Select(d => d.DisplayName).Contains(dto.Role))
+             .OrderBy(d => d.Account.Status)
+             .OrderBy(d => d.Organization.OrgCode)
+             .OrderByDescending(d => d.CreationTime)
+             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+
         return new PagedDto<UserDto>(total, _mapper.Map<IReadOnlyList<UserDto>>(items));
     }
 
@@ -182,12 +189,12 @@ public class UserController : BaseController
     {
         var work = _userCacheManager.GetWorkByUser(id);
         if (work is not null)
-            throw UserFriendlyException.SameMessage("用户正在工作中,请下班以后再删除");
+            throw UserFriendlyException.SameMessage("用户正在工作中,请下班以后再删除");
 
         var account = await _accountRepository.GetAsync(id, HttpContext.RequestAborted);
         if (account is null)
-            throw UserFriendlyException.SameMessage("账号不存在");
-        await _accountDomainService.LockOutAsync(account, cancellationToken: HttpContext.RequestAborted);
+            throw UserFriendlyException.SameMessage("账号不存在");
+        await _accountDomainService.UnRegisterAsync(account, HttpContext.RequestAborted);
         await _userRepository.RemoveAsync(id, true, HttpContext.RequestAborted);
     }
 
@@ -285,8 +292,13 @@ public class UserController : BaseController
         await _accountDomainService.InitialPasswordAsync(account, HttpContext.RequestAborted);
     }
 
+    /// <summary>
+    /// 根据id批量查询用户
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <returns></returns>
     [HttpGet]
-    public async Task<IReadOnlyList<UserDto>> Query([FromQuery]IReadOnlyList<string> ids)
+    public async Task<IReadOnlyList<UserDto>> Query([FromQuery] IReadOnlyList<string> ids)
     {
         var users = await _userRepository.Queryable().ToListAsync(d => ids.Contains(d.Id));
         return _mapper.Map<IReadOnlyList<UserDto>>(users);

+ 17 - 64
src/Hotline.Api/StartupExtensions.cs

@@ -55,83 +55,37 @@ internal static class StartupExtensions
             .AddApplication()
             .AddScoped<IPasswordHasher<Account>, PasswordHasher<Account>>()
             ;
-
+        
         //Authentication
-        services.AddAuthenticationService(configuration);
+        services.RegisterAuthentication(configuration);
 
         services.AddControllers(options =>
         {
             options.Filters.Add<UnifyResponseFilter>();
             options.Filters.Add<UserFriendlyExceptionFilter>();
         })
-            .AddDapr(d =>
-            {
-#if DEBUG
-                d.UseHttpEndpoint("http://192.168.100.223:50112");
-#endif
-            });
+//            .AddDapr(d =>
+//            {
+//#if DEBUG
+//                d.UseHttpEndpoint("http://192.168.100.223:50112");
+//#endif
+//            })
+            ;
 
         // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
         services.AddEndpointsApiExplorer();
-        services.AddSwaggerGen(c =>
-        {
-            //添加文档
-            c.SwaggerDoc("v1", new OpenApiInfo() { Title = "Hotline Api", Version = "v1.0", Description = "城市热线api" });
-            //使用反射获取xml文件,并构造出文件的路径
-            var xmlFile = "document.xml";
-            //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
-            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
-            // 启用xml注释. 该方法第二个参数启用控制器的注释,默认为false.
-            c.IncludeXmlComments(xmlPath, true);
-
-            var scheme = new OpenApiSecurityScheme()
-            {
-                Description = "Authorization header. \r\nExample: 'Bearer ***'",
-                Reference = new OpenApiReference
-                {
-                    Type = ReferenceType.SecurityScheme,
-                    Id = "Authorization"
-                },
-                Scheme = "oauth2",
-                Name = "Authorization",
-                In = ParameterLocation.Header,
-                Type = SecuritySchemeType.ApiKey,
-            };
-            c.AddSecurityDefinition("Authorization", scheme);
-            var requirement = new OpenApiSecurityRequirement();
-            requirement[scheme] = new List<string>();
-            c.AddSecurityRequirement(requirement);
-        });
+
+        //swagger
+        services.RegisterSwagger();
 
         //signalR
-        builder.Services.AddSignalR().AddStackExchangeRedis(configuration.GetConnectionString("Redis"), options =>
-        {
-            options.Configuration.ChannelPrefix = "callcenter:signalR:";
-        });
+        services.RegisterSignalR(configuration);
 
         /* CORS */
-        services.AddCors(options =>
-        {
-            options.AddPolicy(name: CorsOrigins,
-                builder =>
-                {
-                    var origins = configuration.GetSection("Cors:Origins").Get<string[]>();
-                    builder.SetIsOriginAllowed(a =>
-                        {
-                            return origins.Any(origin => origin.StartsWith("*.", StringComparison.Ordinal)
-                                ? a.EndsWith(origin[1..], StringComparison.Ordinal)
-                                : a.Equals(origin, StringComparison.Ordinal));
-                        })
-                        .AllowAnyHeader()
-                        .AllowAnyMethod()
-                        .AllowCredentials();
-                });
-        });
+        services.RegisterCors(configuration, CorsOrigins);
 
         //mapster
-        var config = TypeAdapterConfig.GlobalSettings;
-        services.AddSingleton(config);
-        services.AddScoped<IMapper, ServiceMapper>();
+        services.RegisterMapper();
 
         //mediatr
         services.AddMediatR(Assembly.GetExecutingAssembly(), typeof(ApplicationStartupExtensions).Assembly);
@@ -189,12 +143,11 @@ internal static class StartupExtensions
         app.UseAuthentication();
         app.UseAuthorization();
         app.MapHub<CallCenterHub>("/hubs/callcenter");
-        //app.UseMiddleware<TempTokenMiddleware>();
 
-        app.UseCloudEvents();
+        //app.UseCloudEvents();
         app.MapControllers()
             .RequireAuthorization();
-        app.MapSubscribeHandler();
+        //app.MapSubscribeHandler();
 
         return app;
     }

+ 126 - 1
src/Hotline.Api/StartupHelper.cs

@@ -1,14 +1,23 @@
 using System.IdentityModel.Tokens.Jwt;
 using System.Text;
+using Mapster;
+using MapsterMapper;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.IdentityModel.Tokens;
+using Microsoft.OpenApi.Models;
 using XF.Domain.Options;
 
 namespace Hotline.Api
 {
     public static class StartupHelper
     {
-        public static IServiceCollection AddAuthenticationService(this IServiceCollection services, ConfigurationManager configuration)
+        /// <summary>
+        /// Authentication
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="configuration"></param>
+        /// <returns></returns>
+        public static IServiceCollection RegisterAuthentication(this IServiceCollection services, ConfigurationManager configuration)
         {
             //JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
             //services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
@@ -55,10 +64,126 @@ namespace Hotline.Api
                         ValidateIssuerSigningKey = true,
                         IssuerSigningKey = secKey,
                     };
+
+                    //d.Audience = "hotline_api";
+                    d.Events = new JwtBearerEvents
+                    {
+                        OnMessageReceived = context =>
+                        {
+                            var accessToken = context.Request.Query["access_token"];
+
+                                    // If the request is for our hub...
+                            var path = context.HttpContext.Request.Path;
+                            if (!string.IsNullOrEmpty(accessToken) &&
+                                (path.StartsWithSegments("/hubs/callcenter")))
+                            {
+                                        // Read the token out of the query string
+                                context.Token = accessToken;
+                            }
+                            return Task.CompletedTask;
+                        }
+                    };
                 })
                 ;
 
             return services;
         }
+
+        /// <summary>
+        /// Swagger
+        /// </summary>
+        /// <param name="services"></param>
+        /// <returns></returns>
+        public static IServiceCollection RegisterSwagger(this IServiceCollection services)
+        {
+            services.AddSwaggerGen(c =>
+            {
+                //添加文档
+                c.SwaggerDoc("v1", new OpenApiInfo() { Title = "Hotline Api", Version = "v1.0", Description = "城市热线api" });
+                //使用反射获取xml文件,并构造出文件的路径
+                var xmlFile = "document.xml";
+                //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
+                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+                // 启用xml注释. 该方法第二个参数启用控制器的注释,默认为false.
+                c.IncludeXmlComments(xmlPath, true);
+
+                var scheme = new OpenApiSecurityScheme()
+                {
+                    Description = "Authorization header. \r\nExample: 'Bearer ***'",
+                    Reference = new OpenApiReference
+                    {
+                        Type = ReferenceType.SecurityScheme,
+                        Id = "Authorization"
+                    },
+                    Scheme = "oauth2",
+                    Name = "Authorization",
+                    In = ParameterLocation.Header,
+                    Type = SecuritySchemeType.ApiKey,
+                };
+                c.AddSecurityDefinition("Authorization", scheme);
+                var requirement = new OpenApiSecurityRequirement();
+                requirement[scheme] = new List<string>();
+                c.AddSecurityRequirement(requirement);
+            });
+
+            return services;
+        }
+
+        /// <summary>
+        /// Cors
+        /// </summary>
+        /// <param name="services"></param>
+        /// <returns></returns>
+        public static IServiceCollection RegisterCors(this IServiceCollection services, ConfigurationManager configuration, string corsOrigins)
+        {
+            services.AddCors(options =>
+            {
+                options.AddPolicy(name: corsOrigins,
+                    builder =>
+                    {
+                        var origins = configuration.GetSection("Cors:Origins").Get<string[]>();
+                        builder.SetIsOriginAllowed(a =>
+                            {
+                                return origins.Any(origin => origin.StartsWith("*.", StringComparison.Ordinal)
+                                    ? a.EndsWith(origin[1..], StringComparison.Ordinal)
+                                    : a.Equals(origin, StringComparison.Ordinal));
+                            })
+                            .AllowAnyHeader()
+                            .AllowAnyMethod()
+                            .AllowCredentials();
+                    });
+            });
+
+            return services;
+        }
+
+        /// <summary>
+        /// Mapper
+        /// </summary>
+        /// <param name="services"></param>
+        /// <returns></returns>
+        public static IServiceCollection RegisterMapper(this IServiceCollection services)
+        {
+            var config = TypeAdapterConfig.GlobalSettings;
+            services.AddSingleton(config);
+            services.AddScoped<IMapper, ServiceMapper>();
+
+            return services;
+        }
+
+        /// <summary>
+        /// SignalR
+        /// </summary>
+        /// <param name="services"></param>
+        /// <returns></returns>
+        public static IServiceCollection RegisterSignalR(this IServiceCollection services, ConfigurationManager configuration)
+        {
+            services.AddSignalR().AddStackExchangeRedis(configuration.GetConnectionString("Redis"), options =>
+            {
+                options.Configuration.ChannelPrefix = "callcenter:signalR:";
+            });
+
+            return services;
+        }
     }
 }

+ 1 - 1
src/Hotline.Application.Contracts/Validators/User/AddUserDtoValidator.cs

@@ -4,7 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using FluentValidation;
-using Hotline.Share.Dtos.User;
+using Hotline.Share.Dtos.Users;
 
 namespace Hotline.Application.Contracts.Validators.User
 {

+ 1 - 1
src/Hotline.Application.Contracts/Validators/User/UpdateUserDtoValidator.cs

@@ -1,5 +1,5 @@
 using FluentValidation;
-using Hotline.Share.Dtos.User;
+using Hotline.Share.Dtos.Users;
 
 namespace Hotline.Application.Contracts.Validators.User;
 

+ 0 - 73
src/Hotline.Application/Identity/IIdentityAppService.cs

@@ -1,19 +1,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Security.Claims;
 using System.Text;
 using System.Threading.Tasks;
-using Hotline.Identity.Accounts;
 using Hotline.Share.Dtos.Identity;
-using IdentityModel;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.Extensions.Options;
-using XF.Domain.Authentications;
-using XF.Domain.Dependency;
-using XF.Domain.Exceptions;
-using XF.Domain.Options;
-using XF.Utility.AppIdentityModel;
 
 namespace Hotline.Application.Identity
 {
@@ -21,67 +11,4 @@ namespace Hotline.Application.Identity
     {
         Task<string> LoginAsync(LoginDto dto, CancellationToken cancellationToken);
     }
-
-    public class IdentityAppService : IIdentityAppService, IScopeDependency
-    {
-        private readonly IAccountRepository _accountRepository;
-        private readonly IAccountDomainService _accountDomainService;
-        private readonly IJwtSecurity _jwtSecurity;
-        private readonly IOptionsSnapshot<IdentityConfiguration> _identityOptionsAccessor;
-
-        public IdentityAppService(
-            IAccountRepository accountRepository,
-            IAccountDomainService accountDomainService,
-            IJwtSecurity jwtSecurity,
-            IOptionsSnapshot<IdentityConfiguration> identityOptionsAccessor)
-        {
-            _accountRepository = accountRepository;
-            _accountDomainService = accountDomainService;
-            _jwtSecurity = jwtSecurity;
-            _identityOptionsAccessor = identityOptionsAccessor;
-        }
-
-        public async Task<string> LoginAsync(LoginDto dto, CancellationToken cancellationToken)
-        {
-            var account = await _accountRepository.GetExtAsync(
-                d => d.UserName == dto.UserName,
-                d => d.Includes(x => x.Roles));
-            if (account == null)
-                throw UserFriendlyException.SameMessage("用户名或密码错误!");
-
-            if (account.LockoutEnabled && account.LockoutEnd >= DateTime.Now)
-                throw UserFriendlyException.SameMessage("账号被锁定!");
-
-            var verifyResult = _accountDomainService.VerifyPassword(account, dto.Password);
-            if (verifyResult == PasswordVerificationResult.Failed)
-            {
-                var lockoutOptions = _identityOptionsAccessor.Value.Lockout;
-                account.AccessFailedCount += 1;
-                if (account.LockoutEnabled && account.AccessFailedCount >= lockoutOptions.MaxFailedAccessAttempts)
-                    account.LockoutEnd = DateTime.Now.Add(lockoutOptions.DefaultLockoutTimeSpan);
-                await _accountRepository.UpdateAsync(account, cancellationToken);
-                throw UserFriendlyException.SameMessage("账号名或密码错误!");
-            }
-
-            account.LockoutEnd = null;
-            account.AccessFailedCount = 0;
-            await _accountRepository.UpdateAsync(account, cancellationToken);
-
-            //todo 记录count
-            //todo 发放token
-            var jwtOptions = _identityOptionsAccessor.Value.Jwt;
-            var claims = new List<Claim>
-            {
-                //new(JwtClaimTypes.Id, account.Id),
-                new(JwtClaimTypes.Subject, account.Id),
-                new(JwtClaimTypes.PhoneNumber, account.PhoneNo ?? string.Empty),
-                new(AppClaimTypes.UserDisplayName, account.Name),
-                new(JwtClaimTypes.Scope,jwtOptions.Scope),
-                new(AppClaimTypes.UserPasswordChanged, account.PasswordChanged.ToString()),
-            };
-            claims.AddRange(account.Roles.Select(d => new Claim(JwtClaimTypes.Role, d.Name)));
-            var token = _jwtSecurity.EncodeJwtToken(claims);
-            return token;
-        }
-    }
 }

+ 78 - 0
src/Hotline.Application/Identity/IdentityAppService.cs

@@ -0,0 +1,78 @@
+using System.Security.Claims;
+using Hotline.Identity.Accounts;
+using Hotline.Share.Dtos.Identity;
+using Hotline.Share.Enums.Identity;
+using IdentityModel;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using XF.Domain.Authentications;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Options;
+using XF.Utility.AppIdentityModel;
+
+namespace Hotline.Application.Identity;
+
+public class IdentityAppService : IIdentityAppService, IScopeDependency
+{
+    private readonly IAccountRepository _accountRepository;
+    private readonly IAccountDomainService _accountDomainService;
+    private readonly IJwtSecurity _jwtSecurity;
+    private readonly IOptionsSnapshot<IdentityConfiguration> _identityOptionsAccessor;
+
+    public IdentityAppService(
+        IAccountRepository accountRepository,
+        IAccountDomainService accountDomainService,
+        IJwtSecurity jwtSecurity,
+        IOptionsSnapshot<IdentityConfiguration> identityOptionsAccessor)
+    {
+        _accountRepository = accountRepository;
+        _accountDomainService = accountDomainService;
+        _jwtSecurity = jwtSecurity;
+        _identityOptionsAccessor = identityOptionsAccessor;
+    }
+
+    public async Task<string> LoginAsync(LoginDto dto, CancellationToken cancellationToken)
+    {
+        var account = await _accountRepository.GetExtAsync(
+            d => d.UserName == dto.UserName,
+            d => d.Includes(x => x.Roles));
+        if (account == null)
+            throw UserFriendlyException.SameMessage("用户名或密码错误!");
+
+        if (account.Status != EAccountStatus.Normal)
+            throw UserFriendlyException.SameMessage("用户名或密码错误!");
+
+        if (account.LockoutEnabled && account.LockoutEnd >= DateTime.Now)
+            throw UserFriendlyException.SameMessage("账号被锁定!");
+
+        var verifyResult = _accountDomainService.VerifyPassword(account, dto.Password);
+        if (verifyResult == PasswordVerificationResult.Failed)
+        {
+            var lockoutOptions = _identityOptionsAccessor.Value.Lockout;
+            account.AccessFailedCount += 1;
+            if (account.LockoutEnabled && account.AccessFailedCount >= lockoutOptions.MaxFailedAccessAttempts)
+                account.LockoutEnd = DateTime.Now.Add(lockoutOptions.DefaultLockoutTimeSpan);
+            await _accountRepository.UpdateAsync(account, cancellationToken);
+            throw UserFriendlyException.SameMessage("账号名或密码错误!");
+        }
+
+        account.LockoutEnd = null;
+        account.AccessFailedCount = 0;
+        await _accountRepository.UpdateAsync(account, cancellationToken);
+
+        var jwtOptions = _identityOptionsAccessor.Value.Jwt;
+        var claims = new List<Claim>
+        {
+            //new(JwtClaimTypes.Id, account.Id),
+            new(JwtClaimTypes.Subject, account.Id),
+            new(JwtClaimTypes.PhoneNumber, account.PhoneNo ?? string.Empty),
+            new(AppClaimTypes.UserDisplayName, account.Name),
+            new(JwtClaimTypes.Scope,jwtOptions.Scope),
+            new(AppClaimTypes.UserPasswordChanged, account.PasswordChanged.ToString()),
+        };
+        claims.AddRange(account.Roles.Select(d => new Claim(JwtClaimTypes.Role, d.Name)));
+        var token = _jwtSecurity.EncodeJwtToken(claims);
+        return token;
+    }
+}

+ 2 - 3
src/Hotline.Application/Mappers/MapperConfigs.cs

@@ -1,9 +1,8 @@
 using Hotline.CallCenter.BlackLists;
-using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
 using Hotline.Share.Dtos.CallCenter;
-using Hotline.Share.Dtos.Role;
-using Hotline.Share.Dtos.User;
+using Hotline.Share.Dtos.Roles;
+using Hotline.Share.Dtos.Users;
 using Hotline.Users;
 using Mapster;
 

+ 9 - 0
src/Hotline.Application/Roles/IRoleAppService.cs

@@ -0,0 +1,9 @@
+using Hotline.Share.Dtos.Roles;
+
+namespace Hotline.Application.Roles
+{
+    public interface IRoleAppService
+    {
+        Task<IReadOnlyList<RoleDto>> QueryAsync(string keyword);
+    }
+}

+ 27 - 0
src/Hotline.Application/Roles/RoleAppService.cs

@@ -0,0 +1,27 @@
+using Hotline.Identity.Roles;
+using Hotline.Share.Dtos.Roles;
+using MapsterMapper;
+using XF.Domain.Dependency;
+
+namespace Hotline.Application.Roles;
+
+public class RoleAppService : IRoleAppService, IScopeDependency
+{
+    private readonly IRoleRepository _roleRepository;
+    private readonly IMapper _mapper;
+
+    public RoleAppService(
+        IRoleRepository roleRepository,
+        IMapper mapper)
+    {
+        _roleRepository = roleRepository;
+        _mapper = mapper;
+    }
+
+    public async Task<IReadOnlyList<RoleDto>> QueryAsync(string keyword)
+    {
+        var roles = await _roleRepository.Queryable()
+            .ToListAsync(d => d.DisplayName.Contains(keyword));
+        return _mapper.Map<IReadOnlyList<RoleDto>>(roles);
+    }
+}

+ 6 - 6
src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionFilterBuilder.cs

@@ -25,11 +25,11 @@ public class DataPermissionFilterBuilder : IDataPermissionFilterBuilder, IScopeD
         switch (scheme.QueryFilter)
         {
             case EAuthorityType.Create:
-                return d => d.CreateUserId == userId;
+                return d => d.CreatorId == userId;
             case EAuthorityType.Org:
-                return d => d.AutoOrgCode == scheme.OrgCode;
+                return d => d.CreatorOrgCode == scheme.OrgCode;
             case EAuthorityType.OrgAndBelow:
-                return d => d.AutoOrgCode.StartsWith(scheme.OrgCode);
+                return d => d.CreatorOrgCode.StartsWith(scheme.OrgCode);
             case EAuthorityType.All:
                 return d => true;
             default:
@@ -46,11 +46,11 @@ public class DataPermissionFilterBuilder : IDataPermissionFilterBuilder, IScopeD
         switch (scheme.QueryFilter)
         {
             case EAuthorityType.Create:
-                return d => d.CreateUserId == userId || FlowDataFiltering(d, userId, depCode);
+                return d => d.CreatorId == userId || FlowDataFiltering(d, userId, depCode);
             case EAuthorityType.Org:
-                return d => d.AutoOrgCode == scheme.OrgCode || FlowDataFiltering(d, userId, depCode);
+                return d => d.CreatorOrgCode == scheme.OrgCode || FlowDataFiltering(d, userId, depCode);
             case EAuthorityType.OrgAndBelow:
-                return d => d.AutoOrgCode.StartsWith(scheme.OrgCode) || FlowDataFiltering(d, userId, depCode);
+                return d => d.CreatorOrgCode.StartsWith(scheme.OrgCode) || FlowDataFiltering(d, userId, depCode);
             case EAuthorityType.All:
                 return d => true;
             default:

+ 0 - 14
src/Hotline.Share/Dtos/Role/QueryRolesPagedDto.cs

@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Hotline.Share.Requests;
-
-namespace Hotline.Share.Dtos.Role
-{
-    public record QueryRolesPagedDto : PagedKeywordRequest
-    {
-        public bool IncludeDeleted { get; set; }
-    }
-}

+ 9 - 0
src/Hotline.Share/Dtos/Roles/QueryRolesPagedDto.cs

@@ -0,0 +1,9 @@
+using Hotline.Share.Requests;
+
+namespace Hotline.Share.Dtos.Roles
+{
+    public record QueryRolesPagedDto : PagedKeywordRequest
+    {
+        public bool IncludeDeleted { get; set; }
+    }
+}

+ 2 - 3
src/Hotline.Share/Dtos/Role/RoleAuthorityDto.cs → src/Hotline.Share/Dtos/Roles/RoleAuthorityDto.cs

@@ -1,7 +1,6 @@
-
-using Hotline.Share.Enums;
+using Hotline.Share.Enums;
 
-namespace Hotline.Share.Dtos.Role
+namespace Hotline.Share.Dtos.Roles
 {
     public record RoleAuthorityDto
     {

+ 1 - 7
src/Hotline.Share/Dtos/Role/RoleDto.cs → src/Hotline.Share/Dtos/Roles/RoleDto.cs

@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Hotline.Share.Dtos.Role
+namespace Hotline.Share.Dtos.Roles
 {
     public class RoleDto : AddRoleDto
     {

+ 0 - 14
src/Hotline.Share/Dtos/User/SetUserRolesDto.cs

@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Hotline.Share.Dtos.User
-{
-    public class SetUserRolesDto
-    {
-        public string UserId { get; set; }
-        public ICollection<string> RoleIds { get; set; }
-    }
-}

+ 0 - 52
src/Hotline.Share/Dtos/User/UserDto.cs

@@ -1,52 +0,0 @@
-namespace Hotline.Share.Dtos.User;
-
-
-public record UserDto : AddUserDto
-{
-    public string Id { get; set; }
-
-    public DateTime CreationTime { get; set; }
-}
-
-public record AddUserDto
-{
-    /// <summary>
-    /// 手机号
-    /// </summary>
-    public string? PhoneNo { get; set; }
-
-    /// <summary>
-    /// 用户名(登录账号)
-    /// </summary>
-    public string UserName { get; set; }
-
-    /// <summary>
-    /// 展示名称
-    /// </summary>
-    public string? Name { get; set; }
-
-    /// <summary>
-    /// 工号
-    /// </summary>
-    public string? StaffNo { get; set; }
-
-    /// <summary>
-    /// 默认分机号
-    /// </summary>
-    public string? DefaultTelNo { get; set; }
-
-    /// <summary>
-    /// 组织架构ID
-    /// </summary>
-    public string? OrgId { get; set; }
-
-    /// <summary>
-    /// 组织架构Code
-    /// </summary>
-    public string? OrgCode { get; set; }
-}
-
-public record UpdateUserDto : AddUserDto
-{
-    public string Id { get; set; }
-}

+ 0 - 6
src/Hotline.Share/Dtos/User/UserPagedDto.cs

@@ -1,6 +0,0 @@
-using Hotline.Share.Requests;
-
-namespace Hotline.Share.Dtos.User
-{
-    public record UserPagedDto(string? PhoneNo, string? DisplayName) : PagedRequest;
-}

+ 1 - 7
src/Hotline.Share/Dtos/User/ChangePasswordDto.cs → src/Hotline.Share/Dtos/Users/ChangePasswordDto.cs

@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Hotline.Share.Dtos.User
+namespace Hotline.Share.Dtos.Users
 {
     public class ChangePasswordDto
     {

+ 8 - 0
src/Hotline.Share/Dtos/Users/SetUserRolesDto.cs

@@ -0,0 +1,8 @@
+namespace Hotline.Share.Dtos.Users
+{
+    public class SetUserRolesDto
+    {
+        public string UserId { get; set; }
+        public ICollection<string> RoleIds { get; set; }
+    }
+}

+ 89 - 0
src/Hotline.Share/Dtos/Users/UserDto.cs

@@ -0,0 +1,89 @@
+using Hotline.Share.Enums.Order;
+
+namespace Hotline.Share.Dtos.Users;
+
+
+public record UserDto : AddUserDto
+{
+    public string Id { get; set; }
+
+    public DateTime CreationTime { get; set; }
+}
+
+public record AddUserDto
+{
+    public string UserName { get; set; }
+
+    /// <summary>
+    /// 手机号(冗余)
+    /// </summary>
+    public string? PhoneNo { get; set; }
+
+    /// <summary>
+    /// 展示名称(Identity.DisplayName)
+    /// </summary>
+    public string Name { get; set; }
+
+    public EGender Gender { get; set; }
+
+    /// <summary>
+    /// 工号
+    /// </summary>
+    public string? StaffNo { get; set; }
+
+    /// <summary>
+    /// 部门Id
+    /// </summary>
+    public string? OrgId { get; set; }
+
+    /// <summary>
+    /// 部门编码(冗余)
+    /// </summary>
+    public string? OrgCode { get; set; }
+
+    /// <summary>
+    /// 默认分机号
+    /// </summary>
+    public string? DefaultTelNo { get; set; }
+
+    public string Email { get; set; }
+}
+
+public record UpdateUserDto
+{
+    public string Id { get; set; }
+
+    /// <summary>
+    /// 手机号(冗余)
+    /// </summary>
+    public string? PhoneNo { get; set; }
+
+    /// <summary>
+    /// 展示名称(Identity.DisplayName)
+    /// </summary>
+    public string Name { get; set; }
+
+    public EGender Gender { get; set; }
+
+    /// <summary>
+    /// 工号
+    /// </summary>
+    public string? StaffNo { get; set; }
+
+    /// <summary>
+    /// 部门Id
+    /// </summary>
+    public string? OrgId { get; set; }
+
+    /// <summary>
+    /// 部门编码(冗余)
+    /// </summary>
+    public string? OrgCode { get; set; }
+
+    /// <summary>
+    /// 默认分机号
+    /// </summary>
+    public string? DefaultTelNo { get; set; }
+
+    public string Email { get; set; }
+}

+ 6 - 0
src/Hotline.Share/Dtos/Users/UserPagedDto.cs

@@ -0,0 +1,6 @@
+using Hotline.Share.Requests;
+
+namespace Hotline.Share.Dtos.Users
+{
+    public record UserPagedDto(string? OrgCode, string? Role) : PagedKeywordRequest;
+}

+ 1 - 1
src/Hotline.Share/Dtos/User/UserStateDto.cs → src/Hotline.Share/Dtos/Users/UserStateDto.cs

@@ -1,4 +1,4 @@
-namespace Hotline.Share.Dtos.User
+namespace Hotline.Share.Dtos.Users
 {
     public record UserStateDto(bool IsOnDuty, bool IsResting, string TelNo);
 }

+ 1 - 1
src/Hotline.Share/Dtos/User/WorkDto.cs → src/Hotline.Share/Dtos/Users/WorkDto.cs

@@ -1,4 +1,4 @@
-namespace Hotline.Share.Dtos.User;
+namespace Hotline.Share.Dtos.Users;
 
 public record WorkDto
 {

+ 14 - 0
src/Hotline.Share/Enums/Identity/EAccountStatus.cs

@@ -0,0 +1,14 @@
+namespace Hotline.Share.Enums.Identity;
+
+public enum EAccountStatus
+{
+    /// <summary>
+    /// 正常
+    /// </summary>
+    Normal = 0,
+
+    /// <summary>
+    /// 注销
+    /// </summary>
+    UnRegister = 1,
+}

+ 12 - 0
src/Hotline.Share/Requests/BatchRemoveDto.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Requests;
+
+public class BatchRemoveDto
+{
+    public ICollection<string> Ids { get; set; }
+}

+ 3 - 0
src/Hotline/Identity/Accounts/Account.cs

@@ -1,4 +1,5 @@
 using Hotline.Identity.Roles;
+using Hotline.Share.Enums.Identity;
 using SqlSugar;
 using XF.Domain.Repository;
 
@@ -56,6 +57,8 @@ namespace Hotline.Identity.Accounts
 
         public bool PasswordChanged { get; set; }
 
+        public EAccountStatus Status{ get; set; }
+
         [Navigate(typeof(AccountRole), nameof(AccountRole.AccountId), nameof(AccountRole.RoleId))]
         public List<Role> Roles { get; set; }
     }

+ 13 - 1
src/Hotline/Identity/Accounts/AccountDomainService.cs

@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Identity;
+using Hotline.Share.Enums.Identity;
+using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Options;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
@@ -93,4 +94,15 @@ public class AccountDomainService : IAccountDomainService, IScopeDependency
         account.LockoutEnd = lockoutEnd ?? DateTime.MaxValue;
         await _accountRepository.UpdateAsync(account, cancellationToken);
     }
+
+    public async Task UnRegisterAsync(Account account, CancellationToken cancellationToken)
+    {
+        if (account == null)
+            throw new ArgumentNullException(nameof(account));
+        if (account.Status != EAccountStatus.UnRegister)
+        {
+            account.Status = EAccountStatus.UnRegister;
+            await _accountRepository.UpdateAsync(account, cancellationToken);
+        }
+    }
 }

+ 1 - 0
src/Hotline/Identity/Accounts/IAccountDomainService.cs

@@ -18,5 +18,6 @@ namespace Hotline.Identity.Accounts
         PasswordVerificationResult VerifyPassword(Account account, string pwd);
         bool IsLockedOut(Account account);
         Task LockOutAsync(Account account, DateTime? lockoutEnd = null, CancellationToken cancellationToken = default);
+        Task UnRegisterAsync(Account account, CancellationToken cancellationToken);
     }
 }

+ 1 - 1
src/Hotline/Orders/Order.cs

@@ -19,7 +19,7 @@ namespace Hotline.Orders
         /// <summary>
         /// 服务人姓名(冗余)
         /// </summary>
-        public string ServiceName { get; set; }
+        public string ServerName { get; set; }
 
         /// <summary>
         /// 服务人工号(冗余)

+ 21 - 0
src/Hotline/SeedData/AccountRoleSeedData.cs

@@ -0,0 +1,21 @@
+using Hotline.Identity.Accounts;
+using XF.Domain.Entities;
+
+namespace Hotline.SeedData;
+
+public class AccountRoleSeedData : ISeedData<AccountRole>
+{
+    /// <summary>
+    /// 种子数据
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<AccountRole> HasData() =>
+        new[]
+        {
+            new AccountRole
+            {
+                AccountId = SysAccountSeedData.Id,
+                RoleId = RoleSeedData.Id
+            }
+        };
+}

+ 27 - 0
src/Hotline/SeedData/RoleSeedData.cs

@@ -0,0 +1,27 @@
+using Hotline.Identity.Roles;
+using XF.Domain.Entities;
+
+namespace Hotline.SeedData;
+
+public class RoleSeedData : ISeedData<Role>
+{
+    public static readonly string Id = "08dab1a3-5448-4712-869e-6e2dfd3e0cc6";
+
+    /// <summary>
+    /// 种子数据
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<Role> HasData() =>
+        new[]
+        {
+            new Role
+            {
+                Id = Id,
+                ClientId = "sys",
+                Name = "sysadmin_role",
+                DisplayName = "系统管理员",
+                Description = "系统管理员",
+                CreationTime = DateTime.Now
+            }
+        };
+}

+ 3 - 40
src/Hotline/SeedData/SysAccountSeedData.cs

@@ -4,13 +4,14 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Hotline.Identity.Accounts;
-using Hotline.Identity.Roles;
 using XF.Domain.Entities;
 
 namespace Hotline.SeedData
 {
     public class SysAccountSeedData : ISeedData<Account>
     {
+        public static readonly string Id = "08daa5f2-1878-4cfa-8764-1244f0229994";
+
         /// <summary>
         /// 种子数据
         /// </summary>
@@ -20,7 +21,7 @@ namespace Hotline.SeedData
             {
                 new Account
                 {
-                    Id = "08daa5f2-1878-4cfa-8764-1244f0229994",
+                    Id = Id,
                     ClientId = "sys",
                     UserName = "sysadmin",
                     Name = "初始系统管理账号",
@@ -29,42 +30,4 @@ namespace Hotline.SeedData
                 }
             };
     }
-
-    public class RoleSeedData : ISeedData<Role>
-    {
-        /// <summary>
-        /// 种子数据
-        /// </summary>
-        /// <returns></returns>
-        public IEnumerable<Role> HasData() =>
-            new[]
-            {
-                new Role
-                {
-                    Id = "08dab1a3-5448-4712-869e-6e2dfd3e0cc6",
-                    ClientId = "sys",
-                    Name = "sysadmin_role",
-                    DisplayName = "系统管理员",
-                    Description = "系统管理员",
-                    CreationTime = DateTime.Now
-                }
-            };
-    }
-
-    public class AccountRoleSeedData : ISeedData<AccountRole>
-    {
-        /// <summary>
-        /// 种子数据
-        /// </summary>
-        /// <returns></returns>
-        public IEnumerable<AccountRole> HasData() =>
-            new[]
-            {
-                new AccountRole
-                {
-                    AccountId = "08daa5f2-1878-4cfa-8764-1244f0229994",
-                    RoleId = "08dab1a3-5448-4712-869e-6e2dfd3e0cc6",
-                }
-            };
-    }
 }

+ 22 - 0
src/Hotline/SeedData/UserSeedData.cs

@@ -0,0 +1,22 @@
+using Hotline.Users;
+using XF.Domain.Entities;
+
+namespace Hotline.SeedData;
+
+public class UserSeedData : ISeedData<User>
+{
+    /// <summary>
+    /// 种子数据
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<User> HasData() =>
+        new[]
+        {
+            new User
+            {
+                Id = SysAccountSeedData.Id,
+                Name = "系统管理员",
+                CreationTime = DateTime.Now,
+            }
+        };
+}

+ 1 - 1
src/Hotline/Users/IUserDomainService.cs

@@ -1,5 +1,5 @@
 using Hotline.CallCenter.Tels;
-using Hotline.Share.Dtos.User;
+using Hotline.Share.Dtos.Users;
 
 namespace Hotline.Users
 {

+ 8 - 0
src/Hotline/Users/User.cs

@@ -1,5 +1,8 @@
 using System.ComponentModel;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
 using Hotline.Settings;
+using Hotline.Share.Enums.Order;
 using SqlSugar;
 using XF.Domain.Entities;
 using XF.Domain.Repository;
@@ -23,6 +26,8 @@ namespace Hotline.Users
         /// </summary>
         public string Name { get; set; }
 
+        public EGender Gender { get; set; }
+
         /// <summary>
         /// 工号
         /// </summary>
@@ -53,5 +58,8 @@ namespace Hotline.Users
 
         [Navigate(NavigateType.OneToOne, nameof(OrgId))]
         public SystemOrganize Organization { get; set; }
+
+        [Navigate(NavigateType.OneToOne, nameof(Id))]
+        public Account Account { get; set; }
     }
 }

+ 1 - 1
src/Hotline/Users/UserDomainService.cs

@@ -2,7 +2,7 @@
 using Hotline.CallCenter.Devices;
 using Hotline.CallCenter.Tels;
 using Hotline.Share.Dtos.CallCenter;
-using Hotline.Share.Dtos.User;
+using Hotline.Share.Dtos.Users;
 using MapsterMapper;
 using XF.Domain.Cache;
 using XF.Domain.Dependency;

+ 11 - 9
src/XF.Domain.Repository/Entity.cs

@@ -14,17 +14,19 @@ public abstract class Entity : IEntity<string>, IDomainEvents, IDataPermission,
     /// 组织Id
     /// </summary>
     [SugarColumn(ColumnDescription = "组织Id", IsNullable = true)]
-    public string OrgId { get; private set; }
+    public string? CreatorOrgId { get; private set; }
+
     /// <summary>
     /// 组织编码
     /// </summary>
     [SugarColumn(ColumnDescription = "组织编码", IsNullable = true)]
-    public string AutoOrgCode { get; private set; }
+    public string? CreatorOrgCode { get; private set; }
+
     /// <summary>
     /// 创建人
     /// </summary>
     [SugarColumn(ColumnDescription = "创建人", IsNullable = true)]
-    public string CreateUserId { get; private set; }
+    public string? CreatorId { get; private set; }
 
     /// <summary>
     /// 赋值部门Id
@@ -32,11 +34,11 @@ public abstract class Entity : IEntity<string>, IDomainEvents, IDataPermission,
     [SugarColumn(ColumnDescription = "数据权限区域Id", IsNullable = true)]
     public string AreaId { get; private set; }
 
-    public void CreateDataPermission(string orgId, string departmentCode, string creatorId, string? areaId)
+    public void CreateDataPermission(string orgId, string orgCode, string creatorId, string? areaId)
     {
-        OrgId = OrgId;
-        AutoOrgCode = departmentCode;
-        CreateUserId = creatorId;
+        CreatorOrgId = orgId;
+        CreatorOrgCode = orgCode;
+        CreatorId = creatorId;
         AreaId = areaId ?? string.Empty;
     }
 
@@ -185,9 +187,9 @@ public abstract class PositionWorkflowEntity : PositionEntity, IWorkflow
     [SugarColumn(IsNullable = true)]
     public string? WorkflowId { get; set; }
 
-    [SugarColumn(ColumnDataType = "varchar(4000)", IsJson = true)]
+    [SugarColumn(ColumnDataType = "varchar(3000)", IsJson = true)]
     public List<string> AssignDepCodes { get; set; } = new();
 
-    [SugarColumn(ColumnDataType = "varchar(4000)", IsJson = true)]
+    [SugarColumn(ColumnDataType = "varchar(3000)", IsJson = true)]
     public List<string> AssignUserIds { get; set; } = new();
 }

+ 4 - 4
src/XF.Domain/Entities/IDataPermission.cs

@@ -3,15 +3,15 @@ namespace XF.Domain.Entities
 {
     public interface IDataPermission
     {
-        string OrgId { get; }
+        string CreatorOrgId { get; }
 
-        string AutoOrgCode { get; }
+        string CreatorOrgCode { get; }
 
-        string CreateUserId { get; }
+        string CreatorId { get; }
 
         string AreaId { get; }
 
-        void CreateDataPermission(string orgId,string departmentCode, string creatorId, string? areaId);
+        void CreateDataPermission(string orgId,string orgCode, string creatorId, string? areaId);
     }
 
     public interface IWorkflow