xf 2 年之前
父節點
當前提交
8c5b1dd0f5
共有 49 個文件被更改,包括 950 次插入306 次删除
  1. 5 2
      src/Hotline.Api/Context/DefaultSessionContext.cs
  2. 1 1
      src/Hotline.Api/Controllers/CallController.cs
  3. 28 0
      src/Hotline.Api/Controllers/IdentityController.cs
  4. 1 1
      src/Hotline.Api/Controllers/RoleController.cs
  5. 2 2
      src/Hotline.Api/Controllers/TestController.cs
  6. 7 0
      src/Hotline.Api/Controllers/UserController.cs
  7. 1 1
      src/Hotline.Api/Controllers/WorkflowController.cs
  8. 0 1
      src/Hotline.Api/Hotline.Api.csproj
  9. 0 1
      src/Hotline.Api/Permissions/EPermission.cs
  10. 35 31
      src/Hotline.Api/StartupExtensions.cs
  11. 64 0
      src/Hotline.Api/StartupHelper.cs
  12. 12 1
      src/Hotline.Api/appsettings.Development.json
  13. 19 0
      src/Hotline.Application.Contracts/Validators/Identity/LoginDtoValidator.cs
  14. 1 0
      src/Hotline.Application/Hotline.Application.csproj
  15. 86 0
      src/Hotline.Application/Identity/IIdentityAppService.cs
  16. 5 1
      src/Hotline.Application/Mappers/MapperConfigs.cs
  17. 1 0
      src/Hotline.Repository.SqlSugar/BaseRepository.cs
  18. 1 0
      src/Hotline.Repository.SqlSugar/BaseRepositoryWorkflow.cs
  19. 0 1
      src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionManager.cs
  20. 3 3
      src/Hotline.Repository.SqlSugar/Extensions/DataPermissionExtensions.cs
  21. 113 0
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarExtensions.cs
  22. 1 1
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarRepositoryExtensions.cs
  23. 260 0
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarStartupExtensions.cs
  24. 0 184
      src/Hotline.Repository.SqlSugar/SqlSugarStartupExtensions.cs
  25. 1 1
      src/Hotline.Repository.SqlSugar/Users/UserFastMenuRepository.cs
  26. 2 2
      src/Hotline.Repository.SqlSugar/Users/UserRepository.cs
  27. 2 0
      src/Hotline.Share/Dtos/Identity/LoginDto.cs
  28. 1 0
      src/Hotline.Share/Dtos/Role/RoleDto.cs
  29. 0 1
      src/Hotline/CallCenter/Tels/TelDomainService.cs
  30. 3 2
      src/Hotline/Identity/Accounts/Account.cs
  31. 3 2
      src/Hotline/Identity/Accounts/AccountDomainService.cs
  32. 2 1
      src/Hotline/Identity/Accounts/AccountRole.cs
  33. 1 1
      src/Hotline/Identity/Accounts/IAccountDomainService.cs
  34. 0 14
      src/Hotline/Identity/IIdentityDomainService.cs
  35. 1 0
      src/Hotline/Identity/Roles/Role.cs
  36. 0 2
      src/Hotline/Realtimes/IRealtimeService.cs
  37. 70 0
      src/Hotline/SeedData/SysAccountSeedData.cs
  38. 28 28
      src/Hotline/Settings/SystemOrganize.cs
  39. 1 1
      src/XF.Domain.Repository/Entity.cs
  40. 1 1
      src/XF.Domain.Repository/XF.Domain.Repository.csproj
  41. 77 0
      src/XF.Domain/Authentications/IJwtSecurity.cs
  42. 17 0
      src/XF.Domain/Entities/ISeedData.cs
  43. 12 0
      src/XF.Domain/Entities/ITable.cs
  44. 37 0
      src/XF.Domain/Extensions/TypeDefineExtensions.cs
  45. 14 0
      src/XF.Domain/Options/DatabaseOptions.cs
  46. 29 0
      src/XF.Domain/Options/IdentityConfiguration.cs
  47. 1 0
      src/XF.Domain/Password/IAppPasswordValidator.cs
  48. 0 19
      src/XF.Domain/Password/IdentityConfiguration.cs
  49. 1 0
      src/XF.Domain/XF.Domain.csproj

+ 5 - 2
src/Hotline.Api/Context/DefaultSessionContext.cs

@@ -1,4 +1,5 @@
 using System.Security.Authentication;
+using System.Security.Claims;
 using IdentityModel;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
@@ -15,10 +16,12 @@ namespace Hotline.Api.Token
                 throw new ArgumentNullException(nameof(httpContext));
 
             var user = httpContext.User;
-            UserId = user.FindFirstValue(JwtClaimTypes.Subject);
+            //UserId = user.FindFirstValue(JwtClaimTypes.Id);
+            UserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
             UserName = user.FindFirstValue(AppClaimTypes.UserDisplayName);
             Phone = user.FindFirstValue(JwtClaimTypes.PhoneNumber);
-            Roles = user.Claims.Where(d => d.Type == JwtClaimTypes.Role).Select(d => d.Value).ToArray();
+            //Roles = user.Claims.Where(d => d.Type == JwtClaimTypes.Role).Select(d => d.Value).ToArray();
+            Roles = user.Claims.Where(d => d.Type == ClaimTypes.Role).Select(d => d.Value).ToArray();
         }
 
         /// <summary>

+ 1 - 1
src/Hotline.Api/Controllers/CallController.cs

@@ -1,7 +1,7 @@
 using Hotline.CallCenter.BlackLists;
 using Hotline.CallCenter.Calls;
 using Hotline.Permissions;
-using Hotline.Repository.SqlSugar;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Enums.CallCenter;

+ 28 - 0
src/Hotline.Api/Controllers/IdentityController.cs

@@ -0,0 +1,28 @@
+using Hotline.Application.Identity;
+using Hotline.Share.Dtos.Identity;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Api.Controllers;
+
+public class IdentityController : BaseController
+{
+    private readonly IIdentityAppService _identityAppService;
+
+    public IdentityController(IIdentityAppService identityAppService)
+    {
+        _identityAppService = identityAppService;
+    }
+
+    /// <summary>
+    /// 登录
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [HttpPost("login")]
+    public async Task<string> Login([FromBody] LoginDto dto)
+    {
+        return await _identityAppService.LoginAsync(dto, HttpContext.RequestAborted);
+    }
+}

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

@@ -1,6 +1,6 @@
 using Hotline.Identity.Roles;
 using Hotline.Permissions;
-using Hotline.Repository.SqlSugar;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Role;

+ 2 - 2
src/Hotline.Api/Controllers/TestController.cs

@@ -85,11 +85,11 @@ public class TestController : BaseController
         _accountDomainService = accountDomainService;
     }
 
-    [AllowAnonymous]
+    //[AllowAnonymous]
     [HttpGet("hash")]
     public async Task Hash()
     {
-        
+        var s = _sessionContext;
     }
 
 

+ 7 - 0
src/Hotline.Api/Controllers/UserController.cs

@@ -283,4 +283,11 @@ public class UserController : BaseController
         await _accountDomainService.InitialPasswordAsync(account, HttpContext.RequestAborted);
     }
 
+    [HttpGet]
+    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);
+    }
+
 }

+ 1 - 1
src/Hotline.Api/Controllers/WorkflowController.cs

@@ -1,5 +1,5 @@
 using Hotline.FlowEngine.Definitions;
-using Hotline.Repository.SqlSugar;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Workflow;

+ 0 - 1
src/Hotline.Api/Hotline.Api.csproj

@@ -19,7 +19,6 @@
     <PackageReference Include="Serilog.Sinks.Exceptionless" Version="3.1.5" />
     <PackageReference Include="Serilog.Sinks.MongoDB" Version="5.3.1" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
-    <PackageReference Include="XF.Utility.AppIdentityModel" Version="1.0.2" />
   </ItemGroup>
 
   <ItemGroup>

+ 0 - 1
src/Hotline.Api/Permissions/EPermission.cs

@@ -1,7 +1,6 @@
 // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
 // Licensed under MIT license. See License.txt in the project root for license information.
 
-using Npgsql.TypeHandlers.NetworkHandlers;
 using System.ComponentModel.DataAnnotations;
 
 namespace Hotline.Permissions

+ 35 - 31
src/Hotline.Api/StartupExtensions.cs

@@ -13,6 +13,7 @@ using Hotline.Identity.Accounts;
 using Hotline.NewRock;
 using Hotline.Permissions;
 using Hotline.Repository.SqlSugar;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Identity.Admin.HttpClient;
 using Mapster;
@@ -26,6 +27,7 @@ using Microsoft.OpenApi.Models;
 using Serilog;
 using XF.Domain.Dependency;
 using XF.Domain.Filters;
+using XF.Domain.Options;
 using XF.Domain.Password;
 
 namespace Hotline.Api;
@@ -70,37 +72,39 @@ internal static class StartupExtensions
                 d.ClientScope = identityConfigs.ClientScope;
             });
 
-        JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
-        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
-            .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, d =>
-            {
-                d.Authority = identityConfigs.IdentityUrl;
-                d.RequireHttpsMetadata = false;
-                d.TokenValidationParameters = new TokenValidationParameters
-                {
-                    ValidateAudience = false
-                };
-                //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;
-                    }
-                };
-            })
-            ;
+        //JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
+        //services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+        //    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, d =>
+        //    {
+        //        d.Authority = identityConfigs.IdentityUrl;
+        //        d.RequireHttpsMetadata = false;
+        //        d.TokenValidationParameters = new TokenValidationParameters
+        //        {
+        //            ValidateAudience = false
+        //        };
+        //        //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;
+        //            }
+        //        };
+        //    })
+        //    ;
+
+        services.AddAuthenticationService(configuration);
 
         services.AddControllers(options =>
         {

+ 64 - 0
src/Hotline.Api/StartupHelper.cs

@@ -0,0 +1,64 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Text;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using XF.Domain.Options;
+
+namespace Hotline.Api
+{
+    public static class StartupHelper
+    {
+        public static IServiceCollection AddAuthenticationService(this IServiceCollection services, ConfigurationManager configuration)
+        {
+            //JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
+            //services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+            //    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, d =>
+            //    {
+            //        d.Authority = identityConfigs.IdentityUrl;
+            //        d.RequireHttpsMetadata = false;
+            //        d.TokenValidationParameters = new TokenValidationParameters
+            //        {
+            //            ValidateAudience = false
+            //        };
+            //        //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;
+            //            }
+            //        };
+            //    });
+
+            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+                .AddJwtBearer(d =>
+                {
+                    var jwtOptions = configuration.GetSection("IdentityConfiguration").Get<IdentityConfiguration>().Jwt;
+                    byte[] bytes = Encoding.UTF8.GetBytes(jwtOptions.SecretKey);
+                    var secKey = new SymmetricSecurityKey(bytes);
+                    d.TokenValidationParameters = new()
+                    {
+                        ValidateIssuer = false,
+                        ValidateAudience = false,
+                        ValidateLifetime = true,
+                        ValidateIssuerSigningKey = true,
+                        IssuerSigningKey = secKey,
+                    };
+                })
+                ;
+
+            return services;
+        }
+    }
+}

+ 12 - 1
src/Hotline.Api/appsettings.Development.json

@@ -45,7 +45,7 @@
     "Authorize": true,
     "ReceiveKey": "E1BBD1BB-A269-44",
     "SendKey": "2A-BA92-160A3B1D",
-    "Expired": 1440 //认证过期时间(秒)
+    "Expired": 86300 //认证过期时间(秒)
   },
   "ConnectionStrings": {
     "Hotline": "server=db.fengwo.com;Database=hotline;Uid=dev;Pwd=fengwo11!!;SslMode=none;",
@@ -96,6 +96,17 @@
     },
     "Account": {
       "DefaultPassword": "Fwkj@789"
+    },
+    "Jwt": {
+      "SecretKey": "e660d04ef1d3410798c953f5d7b8a4e1",
+      "Issuer": "hotline_server",
+      "Audience": "hotline",
+      "Scope": "hotline_api",
+      "Expired": 86400 //seceonds
     }
+  },
+  "DatabaseConfiguration": {
+    "ApplyDbMigrations": false,
+    "ApplySeed": false
   }
 }

+ 19 - 0
src/Hotline.Application.Contracts/Validators/Identity/LoginDtoValidator.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentValidation;
+using Hotline.Share.Dtos.Identity;
+
+namespace Hotline.Application.Contracts.Validators.Identity
+{
+    public class LoginDtoValidator : AbstractValidator<LoginDto>
+    {
+        public LoginDtoValidator()
+        {
+            RuleFor(d => d.UserName).NotEmpty();
+            RuleFor(d => d.Password).NotEmpty();
+        }
+    }
+}

+ 1 - 0
src/Hotline.Application/Hotline.Application.csproj

@@ -9,6 +9,7 @@
   <ItemGroup>
     <PackageReference Include="Dapr.AspNetCore" Version="1.9.0" />
     <PackageReference Include="Identity.Admin.HttpClient" Version="1.0.20" />
+    <PackageReference Include="XF.Utility.AppIdentityModel" Version="1.0.2" />
   </ItemGroup>
 
   <ItemGroup>

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

@@ -0,0 +1,86 @@
+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
+{
+    public interface IIdentityAppService
+    {
+        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),
+            };
+            claims.AddRange(account.Roles.Select(d => new Claim(JwtClaimTypes.Role, d.Name)));
+            var token = _jwtSecurity.EncodeJwtToken(claims);
+            return token;
+        }
+    }
+}

+ 5 - 1
src/Hotline.Application/Mappers/MapperConfigs.cs

@@ -1,6 +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.Users;
 using Mapster;
@@ -17,7 +19,9 @@ namespace Hotline.Application.Mappers
 
             config.NewConfig<AddUserDto, User>()
                 .Map(d => d.Name, x => x.Name ?? x.UserName);
-            
+
+            config.NewConfig<Role, RoleDto>()
+                .Map(d => d.AccountIds, x => x.Accounts.Select(d => d.Id));
         }
     }
 }

+ 1 - 0
src/Hotline.Repository.SqlSugar/BaseRepository.cs

@@ -1,5 +1,6 @@
 using System.Linq.Expressions;
 using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Repository.SqlSugar.Extensions;
 using SqlSugar;
 using XF.Domain.Entities;
 using XF.Domain.Repository;

+ 1 - 0
src/Hotline.Repository.SqlSugar/BaseRepositoryWorkflow.cs

@@ -1,5 +1,6 @@
 using System.Linq.Expressions;
 using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Repository.SqlSugar.Extensions;
 using SqlSugar;
 using XF.Domain.Entities;
 using XF.Domain.Repository;

+ 0 - 1
src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionManager.cs

@@ -2,7 +2,6 @@
 using Hotline.Share.Enums;
 using Hotline.Users;
 using Microsoft.Extensions.DependencyInjection;
-using Org.BouncyCastle.Asn1.X509;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using XF.Domain.Entities;

+ 3 - 3
src/Hotline.Repository.SqlSugar/DataPermissionExtensions.cs → src/Hotline.Repository.SqlSugar/Extensions/DataPermissionExtensions.cs

@@ -2,7 +2,7 @@
 using SqlSugar;
 using XF.Domain.Entities;
 
-namespace Hotline.Repository.SqlSugar
+namespace Hotline.Repository.SqlSugar.Extensions
 {
     public static class DataPermissionExtensions
     {
@@ -21,8 +21,8 @@ namespace Hotline.Repository.SqlSugar
         public static TEntity InitDatePermission<TEntity>(this TEntity entity, IDataPermissionManager dataPermissionManager)
             where TEntity : class, IEntity<string>, IDataPermission, new()
         {
-            var (orgId,departmentCode, creatorId, areaId) = dataPermissionManager.GetDataPermissionOptions();
-            entity.CreateDataPermission(orgId,departmentCode, creatorId, areaId);
+            var (orgId, departmentCode, creatorId, areaId) = dataPermissionManager.GetDataPermissionOptions();
+            entity.CreateDataPermission(orgId, departmentCode, creatorId, areaId);
             return entity;
         }
     }

+ 113 - 0
src/Hotline.Repository.SqlSugar/Extensions/SqlSugarExtensions.cs

@@ -0,0 +1,113 @@
+using System.Collections;
+using System.Data;
+using System.Reflection;
+using SqlSugar;
+
+namespace Hotline.Repository.SqlSugar.Extensions
+{
+    public static class SqlSugarExtensions
+    {
+        /// <summary>
+        /// List转Dictionary
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="list"></param>
+        /// <returns></returns>
+        public static List<Dictionary<string, object?>> ToDictionary<T>(this List<T> list)
+        {
+            var result = new List<Dictionary<string, object?>>();
+            if (list.Any())
+            {
+                foreach (var item in list)
+                {
+                    Dictionary<string, object?> dc = new();
+                    var properties = item.GetType().GetProperties();
+                    foreach (var property in properties)
+                    {
+                        if (IsIgnoreColumn(property)) continue;
+                        if (IsNavigateColumn(property)) continue;
+                        dc.Add(property.Name, property.GetValue(item));
+                    }
+                    result.Add(dc);
+                }
+            }
+
+            return result;
+        }
+
+        public static DataTable ToDataTable<T>(this List<T> list, string tableName)
+        {
+            var dt = new DataTable();
+            dt.TableName = tableName; //设置表名
+
+            if (list.Any())
+            {
+                PropertyInfo[] properties = list[0].GetType().GetProperties();
+                foreach (PropertyInfo property in properties)
+                {
+                    if (IsIgnoreColumn(property)) continue;
+                    if (IsNavigateColumn(property)) continue;
+                    Type colType = property.PropertyType;
+                    if (colType.IsGenericType && colType.GetGenericTypeDefinition() == typeof(Nullable<>))
+                    {
+                        colType = colType.GetGenericArguments()[0];
+                    }
+                    dt.Columns.Add(property.Name, colType);
+                }
+
+                foreach (var item in list)
+                {
+                    ArrayList tempList = new();
+                    //var properties = item.GetType().GetProperties();
+                    foreach (var property in properties)
+                    {
+                        if (IsIgnoreColumn(property)) continue;
+                        if (IsNavigateColumn(property)) continue;
+                        var obj = property.GetValue(item, null);
+                        tempList.Add(obj);
+                    }
+                    dt.LoadDataRow(tempList.ToArray(), true);
+                }
+
+                //for (int i = 0; i < list.Count; i++)
+                //{
+                //    ArrayList tempList = new();
+                //    foreach (PropertyInfo pi in propertys)
+                //    {
+                //        if (IsIgnoreColumn(pi))
+                //            continue;
+                //        object obj = pi.GetValue(list[i], null);
+                //        tempList.Add(obj);
+                //    }
+                //    object[] array = tempList.ToArray();
+                //    result.LoadDataRow(array, true);
+                //}
+            }
+
+            //var addRow = dt.NewRow();
+            //addRow["id"] = 0;
+            //addRow["price"] = 1;
+            //addRow["Name"] = "a";
+            //dt.Rows.Add(addRow);//添加数据
+
+            //var x = db.Storageable(dt).WhereColumns("id").ToStorage();//id作为主键
+            //x.AsInsertable.IgnoreColumns("id").ExecuteCommand();//如果是自增要添加IgnoreColumns
+            //x.AsUpdateable.ExecuteCommand();
+            return dt;
+        }
+
+        /// <summary>
+        /// 排除SqlSugar忽略的列
+        /// </summary>
+        /// <param name="pi"></param>
+        /// <returns></returns>
+        private static bool IsIgnoreColumn(PropertyInfo property)
+        {
+            var sc = property.GetCustomAttributes<SugarColumn>(false).FirstOrDefault(u => u.IsIgnore);
+            return sc != null;
+        }
+
+        private static bool IsNavigateColumn(PropertyInfo property) =>
+            property.GetCustomAttributes<Navigate>(false).Any();
+    }
+}

+ 1 - 1
src/Hotline.Repository.SqlSugar/SqlSugarRepositoryExtensions.cs → src/Hotline.Repository.SqlSugar/Extensions/SqlSugarRepositoryExtensions.cs

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
 using SqlSugar;
 using XF.Domain.Entities;
 
-namespace Hotline.Repository.SqlSugar
+namespace Hotline.Repository.SqlSugar.Extensions
 {
     public static class SqlSugarRepositoryExtensions
     {

+ 260 - 0
src/Hotline.Repository.SqlSugar/Extensions/SqlSugarStartupExtensions.cs

@@ -0,0 +1,260 @@
+using System.Collections;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using Hotline.Users;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+using SqlSugar;
+using XF.Domain.Entities;
+using XF.Domain.Extensions;
+using XF.Domain.Options;
+using XF.Utility.SequentialId;
+
+namespace Hotline.Repository.SqlSugar.Extensions
+{
+    public static class SqlSugarStartupExtensions
+    {
+        public static void AddSqlSugar(this IServiceCollection services, IConfiguration configuration, string dbName = "Hotline")
+        {
+            //多租户 new SqlSugarScope(List<ConnectionConfig>,db=>{});
+
+            SqlSugarScope sqlSugar = new SqlSugarScope(new ConnectionConfig()
+            {
+                DbType = DbType.MySql,
+                ConnectionString = configuration.GetConnectionString(dbName),
+                IsAutoCloseConnection = true,
+                ConfigureExternalServices = new ConfigureExternalServices
+                {
+                    EntityService = (property, column) =>
+                    {
+                        var attributes = property.GetCustomAttributes(true); //get all attributes 
+
+                        //if (attributes.Any(it => it is KeyAttribute))// by attribute set primarykey
+                        //{
+                        //    column.IsPrimarykey = true; //有哪些特性可以看 1.2 特性明细
+                        //}
+                        ////可以写多个,这边可以断点调试
+                        //// if (attributes.Any(it => it is NotMappedAttribute))
+                        ////{
+                        ////    column.IsIgnore= true; 
+                        ////}
+                        //if (attributes.Any(it => it is DbNullableAttribute))
+                        //{
+                        //    column.IsNullable = true;
+                        //}
+                        //if (attributes.Any(it => it is DbJsonAttribute))
+                        //{
+                        //    column.DataType = "varchar(3000)";
+                        //    column.IsJson = true;
+                        //}
+                        //if (attributes.Any(it => it is DbLengthAttribute))
+                        //{
+                        //    column.Length = (attributes.First(d => d is DbLengthAttribute) as DbLengthAttribute)?.MaxLength ?? 255;
+                        //}
+                        //if (attributes.Any(it => it is DbIgnoreAttribute))
+                        //{
+                        //    column.IsIgnore = true;
+                        //}
+                        if (property.PropertyType.IsGenericType &&
+                            property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
+                        {
+                            column.IsNullable = true;
+                        }
+
+                        if (column.PropertyName.ToLower() == "id" ||
+                            attributes.Any(it => it is KeyAttribute)) //是id的设为主键
+                        {
+                            column.IsPrimarykey = true;
+                            column.Length = 36;
+                        }
+
+                        //column.ColumnDescription = (attributes.FirstOrDefault(d => d is DescriptionAttribute) as DescriptionAttribute)?.Description ?? string.Empty;
+                    },
+                    EntityNameService = (type, entity) =>
+                    {
+                        var attributes = type.GetCustomAttributes(true);
+                        //if (attributes.Any(it => it is TableAttribute))
+                        //{
+                        //    entity.DbTableName = (attributes.First(it => it is TableAttribute) as TableAttribute).UserName;
+                        //}
+                        entity.DbTableName = entity.DbTableName.ToSnakeCase();
+                        if (attributes.Any(d => d is DescriptionAttribute))
+                        {
+                            entity.TableDescription =
+                                (attributes.First(d => d is DescriptionAttribute) as DescriptionAttribute)
+                                .Description;
+                        }
+                    }
+                }
+            },
+                SetDbAop
+            );
+            ISugarUnitOfWork<HotlineDbContext> context = new SugarUnitOfWork<HotlineDbContext>(sqlSugar);
+
+            context.Db.QueryFilter.Add(new SqlFilterItem
+            {
+                FilterValue = d => new SqlFilterResult { Sql = " IsDeleted=0 " },
+                IsJoinQuery = true
+            });
+            ////全局过滤
+            //var deletionTypes = AppDomain.CurrentDomain.GetAssemblies().ToList()
+            //    .SelectMany(d=>d.GetTypes()).Where(d=>d.GetInterfaces()/*.Any(x=>x == typeof(ISoftDelete))*/);//todo  找出即实现ISoftDelete 又实现IEntity
+            //foreach (var deletionType in deletionTypes)
+            //{
+
+            //}
+
+            InitDatabase(context, configuration);
+
+            services.AddSingleton(context);
+        }
+
+        private static void InitDatabase(ISugarUnitOfWork<HotlineDbContext> context, IConfiguration configuration)
+        {
+            context.Db.DbMaintenance.CreateDatabase();
+
+            var dbOptions = configuration.GetSection("DatabaseConfiguration").Get<DatabaseOptions>() ?? new DatabaseOptions();
+            if (dbOptions.ApplyDbMigrations)
+            {
+                var types = typeof(User).Assembly.GetTypes()
+                    .Where(d => d.GetInterfaces().Any(x => x == typeof(ITable)))
+                    .Distinct()
+                    .ToArray();
+
+                context.Db.CodeFirst.InitTables(types);//根据types创建表
+            }
+
+            if (dbOptions.ApplySeed)
+            {
+                var seedDataTypes = AppDomain.CurrentDomain.GetAssemblies()
+                    .SelectMany(d => d.GetTypes())
+                    .Where(d => !d.IsInterface && !d.IsAbstract && d.IsClass
+                                && d.HasImplementedOf(typeof(ISeedData<>)));
+
+                foreach (var seedType in seedDataTypes)
+                {
+                    var instance = Activator.CreateInstance(seedType);
+
+                    var hasDataMethod = seedType.GetMethod("HasData");
+                    var seedData = ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast<object>();
+                    if (seedData == null) continue;
+
+                    var entityType = seedType.GetInterfaces().First().GetGenericArguments().First();
+                    var tableName = context.Db.EntityMaintenance.GetTableName(entityType);
+
+                    var seedDataTable = seedData.ToList().ToDataTable(tableName);
+                    
+                    if (seedDataTable.Columns.Contains(SqlSugarConst.PrimaryKey))
+                    {
+                        var storage = context.Db.Storageable(seedDataTable)
+                            //.SplitInsert(d => !d.Any())
+                            .WhereColumns(SqlSugarConst.PrimaryKey).ToStorage();
+                        storage.AsInsertable.ExecuteCommand();
+                        //var ignoreUpdate = hasDataMethod.GetCustomAttribute<IgnoreUpdateAttribute>();
+                        //if (ignoreUpdate == null) storage.AsUpdateable.ExecuteCommand();
+                    }
+                    else // 没有主键或者不是预定义的主键(有重复的可能)
+                    {
+                        var storage = context.Db.Storageable(seedDataTable)
+                            .SplitDelete(d=>true)
+                            .ToStorage();
+                        storage.AsInsertable.ExecuteCommand();
+                    }
+                }
+            }
+        }
+
+        #region private
+
+        private static void SetDbAop(SqlSugarClient db)
+        {
+            /***写AOP等方法***/
+            db.Aop.OnLogExecuting = (sql, pars) =>
+            {
+                //Log.Information(sql);
+            };
+            db.Aop.OnError = (exp) =>//SQL报错
+            {
+                //exp.sql 这样可以拿到错误SQL,性能无影响拿到ORM带参数使用的SQL
+                Log.Error("SqlError: {0}", exp.Sql);
+
+                //5.0.8.2 获取无参数化 SQL  对性能有影响,特别大的SQL参数多的,调试使用
+                //UtilMethods.GetSqlString(DbType.SqlServer,exp.sql,exp.parameters)           
+            };
+            //db.Aop.OnExecutingChangeSql = (sql, pars) => //可以修改SQL和参数的值
+            //{
+            //    //sql=newsql
+            //    //foreach (var p in pars) //修改
+            //    //{
+
+            //    //}
+
+            //    return new KeyValuePair<string, SugarParameter[]>(sql, pars);
+            //};
+
+            db.Aop.OnLogExecuted = (sql, p) =>
+            {
+                //执行时间超过1秒
+                if (db.Ado.SqlExecutionTime.TotalSeconds > 1)
+                {
+                    //代码CS文件名
+                    var fileName = db.Ado.SqlStackTrace.FirstFileName;
+                    //代码行数
+                    var fileLine = db.Ado.SqlStackTrace.FirstLine;
+                    //方法名
+                    var FirstMethodName = db.Ado.SqlStackTrace.FirstMethodName;
+                    //db.Ado.SqlStackTrace.MyStackTraceList[1].xxx 获取上层方法的信息
+
+                    Log.Warning("slow query ==> fileName: {fileName}, fileLine: {fileLine}, FirstMethodName: {FirstMethodName}",
+                        fileName, fileLine, FirstMethodName);
+                }
+                //相当于EF的 PrintToMiniProfiler
+            };
+
+            db.Aop.DataExecuting = (oldValue, entityInfo) =>
+            {
+                //inset生效
+                if (entityInfo.PropertyName == "CreationTime" && entityInfo.OperationType == DataFilterType.InsertByObject)
+                {
+                    entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
+                                                      //entityInfo有字段所有参数
+                }
+                //update生效        
+                else if (entityInfo.PropertyName == "LastModificationTime" && entityInfo.OperationType == DataFilterType.UpdateByObject)
+                {
+                    entityInfo.SetValue(DateTime.Now);//修改UpdateTime字段
+                }
+
+                //根据当前列修改另一列 可以么写
+                //if(当前列逻辑==XXX)
+                //var properyDate = entityInfo.EntityValue.GetType().GetProperty("Date");
+                //if(properyDate!=null)
+                //properyDate.SetValue(entityInfo.EntityValue,1);
+
+                else if (entityInfo.EntityColumnInfo.IsPrimarykey
+                         && entityInfo.EntityColumnInfo.PropertyName.ToLower() == "id"
+                         && entityInfo.OperationType == DataFilterType.InsertByObject) //通过主键保证只进一次事件
+                {
+                    var propertyId = entityInfo.EntityValue.GetType().GetProperty("Id");
+                    if (propertyId is not null)
+                    {
+                        var idValue = propertyId.GetValue(entityInfo.EntityValue);
+                        if (idValue is null)
+                            //这样每条记录就只执行一次 
+                            entityInfo.SetValue(SequentialStringGenerator.Create());
+                    }
+
+                }
+            };
+        }
+
+        #endregion
+    }
+
+    public class SqlSugarConst
+    {
+        public static string PrimaryKey = "Id";
+    }
+}

+ 0 - 184
src/Hotline.Repository.SqlSugar/SqlSugarStartupExtensions.cs

@@ -1,184 +0,0 @@
-using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Serilog;
-using SqlSugar;
-using XF.Domain.Entities;
-using XF.Domain.Extensions;
-using XF.Utility.SequentialId;
-
-namespace Hotline.Repository.SqlSugar
-{
-    public static class SqlSugarStartupExtensions
-    {
-        public static void AddSqlSugar(this IServiceCollection services, IConfiguration configuration, string dbName = "Hotline")
-        {
-            //多租户 new SqlSugarScope(List<ConnectionConfig>,db=>{});
-
-            SqlSugarScope sqlSugar = new SqlSugarScope(new ConnectionConfig()
-            {
-                DbType = DbType.MySql,
-                ConnectionString = configuration.GetConnectionString(dbName),
-                IsAutoCloseConnection = true,
-                ConfigureExternalServices = new ConfigureExternalServices
-                {
-                    EntityService = (property, column) =>
-                    {
-                        var attributes = property.GetCustomAttributes(true);//get all attributes 
-
-                        //if (attributes.Any(it => it is KeyAttribute))// by attribute set primarykey
-                        //{
-                        //    column.IsPrimarykey = true; //有哪些特性可以看 1.2 特性明细
-                        //}
-                        ////可以写多个,这边可以断点调试
-                        //// if (attributes.Any(it => it is NotMappedAttribute))
-                        ////{
-                        ////    column.IsIgnore= true; 
-                        ////}
-                        //if (attributes.Any(it => it is DbNullableAttribute))
-                        //{
-                        //    column.IsNullable = true;
-                        //}
-                        //if (attributes.Any(it => it is DbJsonAttribute))
-                        //{
-                        //    column.DataType = "varchar(3000)";
-                        //    column.IsJson = true;
-                        //}
-                        //if (attributes.Any(it => it is DbLengthAttribute))
-                        //{
-                        //    column.Length = (attributes.First(d => d is DbLengthAttribute) as DbLengthAttribute)?.MaxLength ?? 255;
-                        //}
-                        //if (attributes.Any(it => it is DbIgnoreAttribute))
-                        //{
-                        //    column.IsIgnore = true;
-                        //}
-                        if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
-                        {
-                            column.IsNullable = true;
-                        }
-                        if (column.PropertyName.ToLower() == "id" || attributes.Any(it => it is KeyAttribute)) //是id的设为主键
-                        {
-                            column.IsPrimarykey = true;
-                            column.Length = 36;
-                        }
-
-                        //column.ColumnDescription = (attributes.FirstOrDefault(d => d is DescriptionAttribute) as DescriptionAttribute)?.Description ?? string.Empty;
-                    },
-                    EntityNameService = (type, entity) =>
-                    {
-                        var attributes = type.GetCustomAttributes(true);
-                        //if (attributes.Any(it => it is TableAttribute))
-                        //{
-                        //    entity.DbTableName = (attributes.First(it => it is TableAttribute) as TableAttribute).UserName;
-                        //}
-                        entity.DbTableName = entity.DbTableName.ToSnakeCase();
-                        if (attributes.Any(d => d is DescriptionAttribute))
-                        {
-                            entity.TableDescription =
-                                (attributes.First(d => d is DescriptionAttribute) as DescriptionAttribute).Description;
-                        }
-                    }
-                }
-            },
-                db =>
-                {
-                    /***写AOP等方法***/
-                    db.Aop.OnLogExecuting = (sql, pars) =>
-                    {
-                        //Log.Information(sql);
-                    };
-                    db.Aop.OnError = (exp) =>//SQL报错
-                    {
-                        //exp.sql 这样可以拿到错误SQL,性能无影响拿到ORM带参数使用的SQL
-                        Log.Error("SqlError: {0}", exp.Sql);
-
-                        //5.0.8.2 获取无参数化 SQL  对性能有影响,特别大的SQL参数多的,调试使用
-                        //UtilMethods.GetSqlString(DbType.SqlServer,exp.sql,exp.parameters)           
-                    };
-                    //db.Aop.OnExecutingChangeSql = (sql, pars) => //可以修改SQL和参数的值
-                    //{
-                    //    //sql=newsql
-                    //    //foreach (var p in pars) //修改
-                    //    //{
-
-                    //    //}
-
-                    //    return new KeyValuePair<string, SugarParameter[]>(sql, pars);
-                    //};
-
-                    db.Aop.OnLogExecuted = (sql, p) =>
-                    {
-                        //执行时间超过1秒
-                        if (db.Ado.SqlExecutionTime.TotalSeconds > 1)
-                        {
-                            //代码CS文件名
-                            var fileName = db.Ado.SqlStackTrace.FirstFileName;
-                            //代码行数
-                            var fileLine = db.Ado.SqlStackTrace.FirstLine;
-                            //方法名
-                            var FirstMethodName = db.Ado.SqlStackTrace.FirstMethodName;
-                            //db.Ado.SqlStackTrace.MyStackTraceList[1].xxx 获取上层方法的信息
-
-                            Log.Warning("slow query ==> fileName: {fileName}, fileLine: {fileLine}, FirstMethodName: {FirstMethodName}",
-                                fileName, fileLine, FirstMethodName);
-                        }
-                        //相当于EF的 PrintToMiniProfiler
-                    };
-
-                    db.Aop.DataExecuting = (oldValue, entityInfo) =>
-                    {
-                        //inset生效
-                        if (entityInfo.PropertyName == "CreationTime" && entityInfo.OperationType == DataFilterType.InsertByObject)
-                        {
-                            entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
-                            //entityInfo有字段所有参数
-                        }
-                        //update生效        
-                        else if (entityInfo.PropertyName == "LastModificationTime" && entityInfo.OperationType == DataFilterType.UpdateByObject)
-                        {
-                            entityInfo.SetValue(DateTime.Now);//修改UpdateTime字段
-                        }
-
-                        //根据当前列修改另一列 可以么写
-                        //if(当前列逻辑==XXX)
-                        //var properyDate = entityInfo.EntityValue.GetType().GetProperty("Date");
-                        //if(properyDate!=null)
-                        //properyDate.SetValue(entityInfo.EntityValue,1);
-
-                        else if (entityInfo.EntityColumnInfo.IsPrimarykey
-                                 && entityInfo.EntityColumnInfo.PropertyName.ToLower() == "id"
-                                 && entityInfo.OperationType == DataFilterType.InsertByObject) //通过主键保证只进一次事件
-                        {
-                            var propertyId = entityInfo.EntityValue.GetType().GetProperty("Id");
-                            if (propertyId is not null)
-                            {
-                                var idValue = propertyId.GetValue(entityInfo.EntityValue);
-                                if (idValue is null)
-                                    //这样每条记录就只执行一次 
-                                    entityInfo.SetValue(SequentialStringGenerator.Create());
-                            }
-
-                        }
-                    };
-                });
-            ISugarUnitOfWork<HotlineDbContext> context = new SugarUnitOfWork<HotlineDbContext>(sqlSugar);
-
-            context.Db.QueryFilter.Add(new SqlFilterItem
-            {
-                FilterValue = d => new SqlFilterResult { Sql = " IsDeleted=0 " },
-                IsJoinQuery = true
-            }
-            );
-            ////全局过滤
-            //var deletionTypes = AppDomain.CurrentDomain.GetAssemblies().ToList()
-            //    .SelectMany(d=>d.GetTypes()).Where(d=>d.GetInterfaces()/*.Any(x=>x == typeof(ISoftDelete))*/);//todo  找出即实现ISoftDelete 又实现IEntity
-            //foreach (var deletionType in deletionTypes)
-            //{
-
-            //}
-
-            services.AddSingleton<ISugarUnitOfWork<HotlineDbContext>>(context);
-        }
-    }
-}

+ 1 - 1
src/Hotline.Repository.SqlSugar/User/UserFastMenuRepository.cs → src/Hotline.Repository.SqlSugar/Users/UserFastMenuRepository.cs

@@ -3,7 +3,7 @@ using Hotline.Settings;
 using SqlSugar;
 using XF.Domain.Dependency;
 
-namespace Hotline.Repository.SqlSugar.User
+namespace Hotline.Repository.SqlSugar.Users
 {
     public class UserFastMenuRepository : BaseRepository<UserFastMenu>, IUserFastMenuRepository, IScopeDependency
     {

+ 2 - 2
src/Hotline.Repository.SqlSugar/User/UserRepository.cs → src/Hotline.Repository.SqlSugar/Users/UserRepository.cs

@@ -3,9 +3,9 @@ using Hotline.Users;
 using SqlSugar;
 using XF.Domain.Dependency;
 
-namespace Hotline.Repository.SqlSugar.User;
+namespace Hotline.Repository.SqlSugar.Users;
 
-public class UserRepository : BaseRepository<Users.User>, IUserRepository, IScopeDependency
+public class UserRepository : BaseRepository<User>, IUserRepository, IScopeDependency
 {
     public UserRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
     {

+ 2 - 0
src/Hotline.Share/Dtos/Identity/LoginDto.cs

@@ -8,5 +8,7 @@ namespace Hotline.Share.Dtos.Identity
 {
     public class LoginDto
     {
+        public string UserName { get; set; }
+        public string Password { get; set; }
     }
 }

+ 1 - 0
src/Hotline.Share/Dtos/Role/RoleDto.cs

@@ -9,6 +9,7 @@ namespace Hotline.Share.Dtos.Role
     public class RoleDto : AddRoleDto
     {
         public string Id { get; set; }
+        public IReadOnlyList<string> AccountIds { get; set; }
     }
 
     public class AddRoleDto

+ 0 - 1
src/Hotline/CallCenter/Tels/TelDomainService.cs

@@ -1,6 +1,5 @@
 using Hotline.CallCenter.Devices;
 using Hotline.Users;
-using Org.BouncyCastle.Bcpg;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
 

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

@@ -4,6 +4,7 @@ using XF.Domain.Repository;
 
 namespace Hotline.Identity.Accounts
 {
+    [SugarIndex("unique_account_username", nameof(Account.UserName), OrderByType.Desc, true)]
     public class Account : FullStateEntity
     {
         public string UserName { get; set; }
@@ -39,13 +40,13 @@ namespace Hotline.Identity.Accounts
         /// <remarks>
         /// A value in the past means the user is not locked out.
         /// </remarks>
-        public DateTimeOffset? LockoutEnd { get; set; }
+        public DateTime? LockoutEnd { get; set; }
 
         /// <summary>
         /// Gets or sets a flag indicating if the user could be locked out.
         /// </summary>
         /// <value>True if the user could be locked out, otherwise false.</value>
-        public bool LockoutEnabled { get; set; }
+        public bool LockoutEnabled { get; set; } = true;
 
         public int AccessFailedCount { get; set; }
 

+ 3 - 2
src/Hotline/Identity/Accounts/AccountDomainService.cs

@@ -2,6 +2,7 @@
 using Microsoft.Extensions.Options;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
+using XF.Domain.Options;
 using XF.Domain.Password;
 
 namespace Hotline.Identity.Accounts;
@@ -82,14 +83,14 @@ public class AccountDomainService : IAccountDomainService, IScopeDependency
         return account.LockoutEnd >= DateTimeOffset.UtcNow;
     }
 
-    public async Task LockOutAsync(Account account, DateTimeOffset? lockoutEnd = null, CancellationToken cancellationToken = default)
+    public async Task LockOutAsync(Account account, DateTime? lockoutEnd = null, CancellationToken cancellationToken = default)
     {
         if (account == null)
             throw new ArgumentNullException(nameof(account));
         if (!account.LockoutEnabled)
             throw UserFriendlyException.SameMessage("该账号不允许锁定");
 
-        account.LockoutEnd = lockoutEnd ?? DateTimeOffset.MaxValue;
+        account.LockoutEnd = lockoutEnd ?? DateTime.MaxValue;
         await _accountRepository.UpdateAsync(account, cancellationToken);
     }
 }

+ 2 - 1
src/Hotline/Identity/Accounts/AccountRole.cs

@@ -1,8 +1,9 @@
 using SqlSugar;
+using XF.Domain.Entities;
 
 namespace Hotline.Identity.Accounts;
 
-public class AccountRole
+public class AccountRole : ITable, IEntity
 {
     [SugarColumn(IsPrimaryKey = true)]
     public string AccountId { get; set; }

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

@@ -17,6 +17,6 @@ namespace Hotline.Identity.Accounts
         Task<IdentityResult> UpdatePasswordHashAsync(Account account, string newPassword, bool validatePassword = true, CancellationToken cancellationToken = default);
         PasswordVerificationResult VerifyPassword(Account account, string pwd);
         bool IsLockedOut(Account account);
-        Task LockOutAsync(Account account, DateTimeOffset? lockoutEnd = null, CancellationToken cancellationToken = default);
+        Task LockOutAsync(Account account, DateTime? lockoutEnd = null, CancellationToken cancellationToken = default);
     }
 }

+ 0 - 14
src/Hotline/Identity/IIdentityDomainService.cs

@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Hotline.Share.Dtos.Identity;
-
-namespace Hotline.Identity
-{
-    public interface IIdentityDomainService
-    {
-        Task<string> LoginAsync(LoginDto dto);
-    }
-}

+ 1 - 0
src/Hotline/Identity/Roles/Role.cs

@@ -4,6 +4,7 @@ using XF.Domain.Repository;
 
 namespace Hotline.Identity.Roles
 {
+    [SugarIndex("unique_role_name", nameof(Role.Name), OrderByType.Desc, true)]
     public class Role : FullStateEntity
     {
         public string Name { get; set; }

+ 0 - 2
src/Hotline/Realtimes/IRealtimeService.cs

@@ -1,6 +1,4 @@
 using Hotline.Share.Dtos.Realtime;
-using System.Threading;
-using Ubiety.Dns.Core;
 
 namespace Hotline.Realtimes
 {

+ 70 - 0
src/Hotline/SeedData/SysAccountSeedData.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+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>
+    {
+        /// <summary>
+        /// 种子数据
+        /// </summary>
+        /// <returns></returns>
+        public IEnumerable<Account> HasData() =>
+            new[]
+            {
+                new Account
+                {
+                    Id = "08daa5f2-1878-4cfa-8764-1244f0229994",
+                    ClientId = "sys",
+                    UserName = "sysadmin",
+                    Name = "初始系统管理账号",
+                    PasswordHash = "AQAAAAEAACcQAAAAEKTgPA/GB2GnCEX+yTgZl878j9M7MOEHxdEgRIMc6xcErRv/oDsMmJBB5zaxcDmXyw==",
+                    CreationTime = DateTime.Now
+                }
+            };
+    }
+
+    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",
+                }
+            };
+    }
+}

+ 28 - 28
src/Hotline/Settings/SystemOrganize.cs

@@ -4,37 +4,37 @@ using System.ComponentModel;
 using XF.Domain.Entities;
 using XF.Domain.Repository;
 
-namespace Hotline.Settings
+namespace Hotline.Settings;
+
+[SugarIndex("unique_org_code", nameof(SystemOrganize.OrgCode), OrderByType.Desc, true)]
+[Description("组织架构")]
+public class SystemOrganize: CreationEntity
 {
-    [Description("组织架构")]
-    public class SystemOrganize: CreationEntity
-    {
-        /// <summary>
-        /// 组织架构名称
-        /// </summary>
-        public string OrgName { get; set; }
+    /// <summary>
+    /// 组织架构名称
+    /// </summary>
+    public string OrgName { get; set; }
 
-        /// <summary>
-        /// 组织架构Code(001001001) 解析:001,001,001)
-        /// </summary>
-        public string OrgCode { get; set; }
+    /// <summary>
+    /// 组织架构Code(001001001) 解析:001,001,001)
+    /// </summary>
+    public string OrgCode { get; set; }
 
-        /// <summary>
-        /// 上级ID
-        /// </summary>
-        public string ParentId { get; set; }
+    /// <summary>
+    /// 上级ID
+    /// </summary>
+    public string ParentId { get; set; }
 
-        /// <summary>
-        /// 上级名称
-        /// </summary>
-        public string ParentName { get; set; }
+    /// <summary>
+    /// 上级名称
+    /// </summary>
+    public string ParentName { get; set; }
 
-        /// <summary>
-        /// 是否启用
-        /// </summary>
-        public bool IsEnable { get; set; }
+    /// <summary>
+    /// 是否启用
+    /// </summary>
+    public bool IsEnable { get; set; }
 
-        [SugarColumn(IsIgnore = true)]
-        public List<SystemOrganize> children { get; set; }
-    }
-}
+    [SugarColumn(IsIgnore = true)]
+    public List<SystemOrganize> children { get; set; }
+}

+ 1 - 1
src/XF.Domain.Repository/Entity.cs

@@ -4,7 +4,7 @@ using XF.Domain.Events;
 
 namespace XF.Domain.Repository;
 
-public abstract class Entity : IEntity<string>, IDomainEvents, IDataPermission
+public abstract class Entity : IEntity<string>, IDomainEvents, IDataPermission, ITable
 {
     private List<IAppNotification> _domainEvents = new();
 

+ 1 - 1
src/XF.Domain.Repository/XF.Domain.Repository.csproj

@@ -9,7 +9,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="SqlSugarCore" Version="5.1.3.32" />
+    <PackageReference Include="SqlSugarCore" Version="5.1.3.40" />
   </ItemGroup>
 
   <ItemGroup>

+ 77 - 0
src/XF.Domain/Authentications/IJwtSecurity.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Authentication;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Options;
+
+namespace XF.Domain.Authentications
+{
+    public interface IJwtSecurity
+    {
+        string EncodeJwtToken(ICollection<Claim> claims);
+        void DecodeJwtToken(string jwt);
+    }
+
+    public class JwtSecurity : IJwtSecurity, IScopeDependency
+    {
+        private readonly IOptions<IdentityConfiguration> _jwtConfigOptionAccessor;
+        private readonly IHttpContextAccessor _contextAccessor;
+
+        public JwtSecurity(IOptions<IdentityConfiguration> jwtConfigOptionAccessor, IHttpContextAccessor contextAccessor)
+        {
+            _jwtConfigOptionAccessor = jwtConfigOptionAccessor;
+            _contextAccessor = contextAccessor;
+        }
+
+        public string EncodeJwtToken(ICollection<Claim> claims)
+        {
+            var jwtOptions = _jwtConfigOptionAccessor.Value.Jwt;
+            if (jwtOptions == null)
+                throw new ArgumentNullException(nameof(jwtOptions));
+
+            var bytes = Encoding.UTF8.GetBytes(jwtOptions.SecretKey);
+            var securityKey = new SymmetricSecurityKey(bytes);
+            var signingCredentials =
+                new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
+            var expired = DateTime.Now.AddMinutes(jwtOptions.Expired);
+            var jwtSecurityToken = new JwtSecurityToken(jwtOptions.Issuer, jwtOptions.Audience, claims, DateTime.Now, expired,
+                signingCredentials);
+            var tokenHandler = new JwtSecurityTokenHandler();
+            var token = tokenHandler.WriteToken(jwtSecurityToken);
+            return token;
+        }
+
+        public void DecodeJwtToken(string jwt)
+        {
+            var jwtOptions = _jwtConfigOptionAccessor.Value.Jwt;
+            if (jwtOptions == null)
+                throw new ArgumentNullException(nameof(jwtOptions));
+
+            JwtSecurityTokenHandler tokenHandler = new();
+            TokenValidationParameters valParam = new();
+            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecretKey));
+            valParam.IssuerSigningKey = securityKey;
+            valParam.ValidateIssuer = false;
+            valParam.ValidateAudience = false;
+            ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, valParam, out SecurityToken secToken);
+            //foreach (var claim in claimsPrincipal.Claims)
+            //{
+            //    Console.WriteLine($"{claim.Type}={claim.Value}");
+
+            //}
+            if (_contextAccessor.HttpContext is null)
+                throw new AuthenticationException($"{nameof(_contextAccessor.HttpContext)} is null");
+            _contextAccessor.HttpContext.User = claimsPrincipal;
+        }
+    }
+}

+ 17 - 0
src/XF.Domain/Entities/ISeedData.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XF.Domain.Entities
+{
+    public interface ISeedData<TEntity> where TEntity : class, IEntity, new()
+    {
+        /// <summary>
+        /// 种子数据
+        /// </summary>
+        /// <returns></returns>
+        IEnumerable<TEntity> HasData();
+    }
+}

+ 12 - 0
src/XF.Domain/Entities/ITable.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XF.Domain.Entities
+{
+    public interface ITable
+    {
+    }
+}

+ 37 - 0
src/XF.Domain/Extensions/TypeDefineExtensions.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XF.Domain.Extensions
+{
+    public static class TypeDefineExtensions
+    {
+        /// <summary>
+        /// 判断类型是否实现某个泛型
+        /// </summary>
+        /// <param name="type">类型</param>
+        /// <param name="generic">泛型类型</param>
+        /// <returns>bool</returns>
+        public static bool HasImplementedOf(this Type type, Type generic)
+        {
+            // 检查接口类型
+            var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType);
+            if (isTheRawGenericType) return true;
+
+            // 检查类型
+            while (type != null && type != typeof(object))
+            {
+                isTheRawGenericType = IsTheRawGenericType(type);
+                if (isTheRawGenericType) return true;
+                type = type.BaseType;
+            }
+
+            return false;
+
+            // 判断逻辑
+            bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type);
+        }
+    }
+}

+ 14 - 0
src/XF.Domain/Options/DatabaseOptions.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XF.Domain.Options
+{
+    public class DatabaseOptions
+    {
+        public bool ApplyDbMigrations { get; set; }
+        public bool ApplySeed { get; set; }
+    }
+}

+ 29 - 0
src/XF.Domain/Options/IdentityConfiguration.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+
+namespace XF.Domain.Options
+{
+    public class IdentityConfiguration : IdentityOptions
+    {
+        public AccountOptions Account { get; set; }
+        public JwtOptions Jwt { get; set; }
+    }
+
+    public class AccountOptions
+    {
+        public string DefaultPassword { get; set; }
+    }
+
+    public class JwtOptions
+    {
+        public string SecretKey { get; set; }
+        public string Issuer { get; set; }
+        public string Audience { get; set; }
+        public string Scope { get; set; }
+        public int Expired { get; set; }
+    }
+}

+ 1 - 0
src/XF.Domain/Password/IAppPasswordValidator.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Options;
 using XF.Domain.Dependency;
+using XF.Domain.Options;
 
 namespace XF.Domain.Password
 {

+ 0 - 19
src/XF.Domain/Password/IdentityConfiguration.cs

@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Identity;
-
-namespace XF.Domain.Password
-{
-    public class IdentityConfiguration : IdentityOptions
-    {
-        public AccountOptions Account { get; set; }
-    }
-
-    public record AccountOptions
-    {
-        public string DefaultPassword { get; set; }
-    }
-}

+ 1 - 0
src/XF.Domain/XF.Domain.csproj

@@ -13,6 +13,7 @@
     <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
     <PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
     <PackageReference Include="Serilog.Enrichers.Span" Version="2.3.0" />
+    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
     <PackageReference Include="XF.Utility.UnifyResponse" Version="1.0.5" />
   </ItemGroup>