Parcourir la source

通话限制配置迁移到数据库

dss il y a 2 ans
Parent
commit
e54c2ac148
64 fichiers modifiés avec 1639 ajouts et 512 suppressions
  1. 5 2
      src/Hotline.Api/Context/DefaultSessionContext.cs
  2. 1 1
      src/Hotline.Api/Controllers/CallController.cs
  3. 1 1
      src/Hotline.Api/Controllers/HomeController.cs
  4. 28 0
      src/Hotline.Api/Controllers/IdentityController.cs
  5. 44 33
      src/Hotline.Api/Controllers/RoleController.cs
  6. 41 30
      src/Hotline.Api/Controllers/TestController.cs
  7. 63 115
      src/Hotline.Api/Controllers/UserController.cs
  8. 1 1
      src/Hotline.Api/Controllers/WorkflowController.cs
  9. 0 1
      src/Hotline.Api/Hotline.Api.csproj
  10. 0 1
      src/Hotline.Api/Permissions/EPermission.cs
  11. 0 1
      src/Hotline.Api/Realtimes/CallCenterHub.cs
  12. 41 33
      src/Hotline.Api/StartupExtensions.cs
  13. 64 0
      src/Hotline.Api/StartupHelper.cs
  14. 34 2
      src/Hotline.Api/appsettings.Development.json
  15. 4 0
      src/Hotline.Application.Contracts/Hotline.Application.Contracts.csproj
  16. 19 0
      src/Hotline.Application.Contracts/Validators/Identity/LoginDtoValidator.cs
  17. 1 0
      src/Hotline.Application/Hotline.Application.csproj
  18. 86 0
      src/Hotline.Application/Identity/IIdentityAppService.cs
  19. 5 0
      src/Hotline.Application/Mappers/MapperConfigs.cs
  20. 20 8
      src/Hotline.Repository.SqlSugar/BaseRepository.cs
  21. 7 1
      src/Hotline.Repository.SqlSugar/BaseRepositoryWorkflow.cs
  22. 0 1
      src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionManager.cs
  23. 3 3
      src/Hotline.Repository.SqlSugar/Extensions/DataPermissionExtensions.cs
  24. 113 0
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarExtensions.cs
  25. 1 1
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarRepositoryExtensions.cs
  26. 260 0
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarStartupExtensions.cs
  27. 33 0
      src/Hotline.Repository.SqlSugar/Identity/AccountRepository.cs
  28. 19 0
      src/Hotline.Repository.SqlSugar/Identity/RoleRepository.cs
  29. 0 184
      src/Hotline.Repository.SqlSugar/SqlSugarStartupExtensions.cs
  30. 0 17
      src/Hotline.Repository.SqlSugar/User/OrgUserRepository.cs
  31. 1 1
      src/Hotline.Repository.SqlSugar/Users/UserFastMenuRepository.cs
  32. 2 2
      src/Hotline.Repository.SqlSugar/Users/UserRepository.cs
  33. 14 0
      src/Hotline.Share/Dtos/Identity/LoginDto.cs
  34. 14 0
      src/Hotline.Share/Dtos/Role/QueryRolesPagedDto.cs
  35. 34 0
      src/Hotline.Share/Dtos/Role/RoleDto.cs
  36. 2 2
      src/Hotline.Share/Dtos/User/ChangePasswordDto.cs
  37. 5 2
      src/Hotline.Share/Dtos/User/SetUserRolesDto.cs
  38. 5 0
      src/Hotline.Share/Requests/PagedRequest.cs
  39. 0 1
      src/Hotline/CallCenter/Tels/TelDomainService.cs
  40. 62 0
      src/Hotline/Identity/Accounts/Account.cs
  41. 96 0
      src/Hotline/Identity/Accounts/AccountDomainService.cs
  42. 13 0
      src/Hotline/Identity/Accounts/AccountRole.cs
  43. 22 0
      src/Hotline/Identity/Accounts/IAccountDomainService.cs
  44. 14 0
      src/Hotline/Identity/Accounts/IAccountRepository.cs
  45. 13 0
      src/Hotline/Identity/Roles/IRoleRepository.cs
  46. 23 0
      src/Hotline/Identity/Roles/Role.cs
  47. 0 2
      src/Hotline/Realtimes/IRealtimeService.cs
  48. 70 0
      src/Hotline/SeedData/SysAccountSeedData.cs
  49. 0 8
      src/Hotline/Settings/IOrgUserRepository.cs
  50. 0 25
      src/Hotline/Settings/OrgUser.cs
  51. 28 28
      src/Hotline/Settings/SystemOrganize.cs
  52. 20 0
      src/Hotline/Users/User.cs
  53. 1 1
      src/XF.Domain.Repository/Entity.cs
  54. 7 2
      src/XF.Domain.Repository/IRepositorySqlSugar.cs
  55. 1 1
      src/XF.Domain.Repository/IRepositoryWorkflow.cs
  56. 1 1
      src/XF.Domain.Repository/XF.Domain.Repository.csproj
  57. 77 0
      src/XF.Domain/Authentications/IJwtSecurity.cs
  58. 17 0
      src/XF.Domain/Entities/ISeedData.cs
  59. 12 0
      src/XF.Domain/Entities/ITable.cs
  60. 37 0
      src/XF.Domain/Extensions/TypeDefineExtensions.cs
  61. 14 0
      src/XF.Domain/Options/DatabaseOptions.cs
  62. 29 0
      src/XF.Domain/Options/IdentityConfiguration.cs
  63. 110 0
      src/XF.Domain/Password/IAppPasswordValidator.cs
  64. 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;

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

@@ -75,7 +75,7 @@ public class HomeController : BaseController
         db.CodeFirst.InitTables<Blacklist,Call,CallDetail,CallRecord,TelHold>();
         db.CodeFirst.InitTables<Ivr, IvrCategory, Tel, TelGroup, TelRest>();
         db.CodeFirst.InitTables<Knowledge, KnowledgeApply, KnowledgePv, KnowledgeStandard, KnowledgeTemp>();
-        db.CodeFirst.InitTables<KnowledgeType, Orders.Order, OrderTemporary, OrgUser>();
+        //db.CodeFirst.InitTables<KnowledgeType, Orders.Order, OrderTemporary, OrgUser>();
         db.CodeFirst.InitTables<SystemAuthority, SystemButton, SystemDataAuthority, SystemDataTable, SystemMenu>();
         db.CodeFirst.InitTables<SystemOrganize, SystemSetting, SystemSettingGroup, UserFastMenu>();
         db.CodeFirst.InitTables<User, Work>();

+ 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);
+    }
+}

+ 44 - 33
src/Hotline.Api/Controllers/RoleController.cs

@@ -1,10 +1,10 @@
-using Hotline.Permissions;
+using Hotline.Identity.Roles;
+using Hotline.Permissions;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Role;
-using Identity.Admin.HttpClient;
-using Identity.Shared.Dtos.Identity;
-using Identity.Shared.Dtos.Role;
+using Hotline.Share.Requests;
 using MapsterMapper;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
@@ -18,17 +18,18 @@ namespace Hotline.Api.Controllers;
 /// </summary>
 public class RoleController : BaseController
 {
-    private readonly IIdentityClient _identityClient;
+    private readonly IRoleRepository _roleRepository;
     private readonly ISystemAuthorityRepository _systemAuthorityRepository;
     private readonly ISystemDataAuthorityRepository _systemDataAuthorityRepository;
     private readonly IMapper _mapper;
 
-    public RoleController(IIdentityClient identityClient, 
-            ISystemAuthorityRepository systemAuthorityRepository,
-            ISystemDataAuthorityRepository systemDataAuthorityRepository,
-            IMapper mapper)
+    public RoleController(
+        IRoleRepository roleRepository,
+        ISystemAuthorityRepository systemAuthorityRepository,
+        ISystemDataAuthorityRepository systemDataAuthorityRepository,
+        IMapper mapper)
     {
-        _identityClient = identityClient;
+        _roleRepository = roleRepository;
         _systemAuthorityRepository = systemAuthorityRepository;
         _systemDataAuthorityRepository = systemDataAuthorityRepository;
         _mapper = mapper;
@@ -41,12 +42,15 @@ public class RoleController : BaseController
     /// <returns></returns>
     [HttpGet("paged")]
     [Permission(EPermission.QueryPagedRole)]
-    public async Task<PagedDto<IdentityRoleDto>> QueryPaged([FromQuery] QueryRolesPagedDto dto)
+    public async Task<PagedDto<RoleDto>> QueryPaged([FromQuery] QueryRolesPagedDto dto)
     {
-        var getRolesRsp = await _identityClient.GetRolesPagedAsync(dto, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(getRolesRsp, "GetRolesPagedAsync");
-        var result = getRolesRsp.Result;
-        return new PagedDto<IdentityRoleDto>(result.TotalCount, result.Roles);
+        var (total, items) = await _roleRepository.Queryable(includeDeleted: dto.IncludeDeleted)
+            .Includes(d => d.Accounts)
+            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Name.Contains(dto.Keyword!) || d.DisplayName.Contains(dto.Keyword!))
+            .OrderByDescending(d => d.CreationTime)
+            .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+
+        return new PagedDto<RoleDto>(total, _mapper.Map<IReadOnlyList<RoleDto>>(items));
     }
 
     /// <summary>
@@ -56,31 +60,39 @@ public class RoleController : BaseController
     /// <returns></returns>
     [Permission(EPermission.AddRole)]
     [HttpPost]
-    public async Task<string> Add([FromBody] IdentityRoleDto dto)
+    public async Task<string> Add([FromBody] AddRoleDto dto)
     {
-        var existsRsp = await _identityClient.IsRoleExistsAsync(dto.Name, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(existsRsp, "IsRoleExistsAsync");
-        if (existsRsp.Result)
-            throw UserFriendlyException.SameMessage("角色名重复");
-
-        var addRoleRsp = await _identityClient.AddRoleAsync(dto, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(addRoleRsp, "AddRoleAsync");
-        return addRoleRsp.Result;
+        dto.ClientId = "hotline_server";
+        var exists = await _roleRepository.Queryable(includeDeleted: true)
+            .AnyAsync(d => d.ClientId == dto.ClientId && d.Name == dto.Name);
+        if (exists)
+            throw UserFriendlyException.SameMessage("角色编码重复");
+
+        return await _roleRepository.AddAsync(_mapper.Map<Role>(dto), HttpContext.RequestAborted);
     }
 
     /// <summary>
     /// 删除角色
     /// </summary>
-    /// <param name="roleId"></param>
+    /// <param name="id"></param>
     /// <returns></returns>
     [Permission(EPermission.RemoveRole)]
-    [HttpDelete("{roleId}")]
-    public async Task Remove(string roleId)
+    [HttpDelete("{id}")]
+    public async Task Remove(string id)
     {
-        var delRoleRsp = await _identityClient.DeleteRoleAsync(roleId, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(delRoleRsp, "DeleteRoleAsync");
-        await _systemAuthorityRepository.RemoveAsync(roleId);
-        await _systemDataAuthorityRepository.RemoveAsync(x=>x.RoleId== roleId);
+        var role = await _roleRepository.Queryable()
+            .Includes(d=>d.Accounts)
+            .FirstAsync(d=>d.Id == id);
+
+        role.Accounts.Clear();
+        await _roleRepository.UpdateNav(role)
+            .Include(d => d.Accounts)
+            .ExecuteCommandAsync();
+
+        await _systemAuthorityRepository.RemoveAsync(id);
+        await _systemDataAuthorityRepository.RemoveAsync(x => x.RoleId == id);
+        
+        await _roleRepository.RemoveAsync(id, true, HttpContext.RequestAborted);
     }
 
     /// <summary>
@@ -92,8 +104,7 @@ public class RoleController : BaseController
     [HttpPut]
     public async Task Update([FromBody] UpdateRoleDto dto)
     {
-        var updateRoleRsp = await _identityClient.UpdateRoleAsync(dto, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(updateRoleRsp, "UpdateRoleAsync");
+        await _roleRepository.UpdateAsync(_mapper.Map<Role>(dto), HttpContext.RequestAborted);
     }
 
     #region 应用权限管理

+ 41 - 30
src/Hotline.Api/Controllers/TestController.cs

@@ -3,6 +3,7 @@ using Hotline.CallCenter.BlackLists;
 using Hotline.CallCenter.Devices;
 using Hotline.CallCenter.Ivrs;
 using Hotline.FlowEngine.Definitions;
+using Hotline.Identity.Accounts;
 using Hotline.Realtimes;
 using Hotline.Repository.SqlSugar;
 using Hotline.Share.Dtos.Realtime;
@@ -36,6 +37,7 @@ public class TestController : BaseController
     private readonly IBlacklistDomainService _blacklistDomainService;
     private readonly IIvrDomainService _ivrDomainService;
     private readonly ISugarUnitOfWork<HotlineDbContext> _uow;
+    private readonly IAccountDomainService _accountDomainService;
 
     //private readonly ITypedCache<List<User>> _cache;
     //private readonly ICacheManager<User> _cache;
@@ -66,7 +68,8 @@ public class TestController : BaseController
         IRealtimeService realtimeService,
         IBlacklistDomainService blacklistDomainService,
         IIvrDomainService ivrDomainService,
-        ISugarUnitOfWork<HotlineDbContext> uow
+        ISugarUnitOfWork<HotlineDbContext> uow,
+        IAccountDomainService accountDomainService
     )
     {
         _logger = logger;
@@ -79,38 +82,46 @@ public class TestController : BaseController
         _blacklistDomainService = blacklistDomainService;
         _ivrDomainService = ivrDomainService;
         _uow = uow;
+        _accountDomainService = accountDomainService;
     }
 
+    //[AllowAnonymous]
+    [HttpGet("hash")]
+    public async Task Hash()
+    {
+        var s = _sessionContext;
+    }
+
+
+    /// <summary>
+    /// signalR测试(method: Ring)
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("ring")]
+    public async Task RingTest()
+    {
+        await _realtimeService.RingAsync(_sessionContext.RequiredUserId, new RingDto { Id = new Guid().ToString(), From = _sessionContext.Phone ?? "未知号码", To = "12345" }, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// signalR测试(method: Answered)
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("answered")]
+    public async Task AnsweredTest()
+    {
+        await _realtimeService.AnsweredAsync(_sessionContext.RequiredUserId, new AnsweredDto() { Id = new Guid().ToString(), From = _sessionContext.Phone ?? "未知号码", To = "12345" }, HttpContext.RequestAborted);
+    }
 
-        /// <summary>
-        /// signalR测试(method: Ring)
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("ring")]
-        public async Task RingTest()
-        {
-            await _realtimeService.RingAsync(_sessionContext.RequiredUserId, new RingDto { Id= new Guid().ToString(), From = _sessionContext.Phone ?? "未知号码",To="12345" }, HttpContext.RequestAborted);
-        }
-
-        /// <summary>
-        /// signalR测试(method: Answered)
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("answered")]
-        public async Task AnsweredTest()
-        {
-            await _realtimeService.AnsweredAsync(_sessionContext.RequiredUserId, new AnsweredDto() { Id = new Guid().ToString(), From = _sessionContext.Phone ?? "未知号码", To = "12345" }, HttpContext.RequestAborted);
-        }
-
-        /// <summary>
-        /// signalR测试(method: Bye)
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("bye")]
-        public async Task ByeTest()
-        {
-            await _realtimeService.ByeAsync(_sessionContext.RequiredUserId, new ByeDto() { Id = new Guid().ToString() }, HttpContext.RequestAborted);
-        }
+    /// <summary>
+    /// signalR测试(method: Bye)
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("bye")]
+    public async Task ByeTest()
+    {
+        await _realtimeService.ByeAsync(_sessionContext.RequiredUserId, new ByeDto() { Id = new Guid().ToString() }, HttpContext.RequestAborted);
+    }
 
     /// <summary>
     /// 

+ 63 - 115
src/Hotline.Api/Controllers/UserController.cs

@@ -1,25 +1,16 @@
-using Hotline.Application.Contracts.Configurations;
-using Hotline.Caches;
+using Hotline.Caches;
 using Hotline.CallCenter.Tels;
+using Hotline.Identity.Accounts;
 using Hotline.Permissions;
 using Hotline.Share.Dtos.User;
 using Hotline.Users;
-using Identity.Admin.HttpClient;
-using Identity.Shared.Dtos;
-using Identity.Shared.Dtos.Account;
-using Identity.Shared.Dtos.Identity;
-using Identity.Shared.Dtos.Role;
 using MapsterMapper;
 using Microsoft.AspNetCore.Mvc;
 using XF.Domain.Authentications;
 using XF.Domain.Exceptions;
-using Microsoft.Extensions.Options;
 using XF.Utility.AppIdentityModel;
-using XF.Utility.UnifyResponse;
-using Hotline.Settings;
-using Microsoft.AspNetCore.Mvc.Formatters;
-using Microsoft.AspNetCore.Authorization;
 using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Role;
 
 namespace Hotline.Api.Controllers;
 
@@ -34,10 +25,9 @@ public class UserController : BaseController
     private readonly IUserRepository _userRepository;
     private readonly ITelCacheManager _telCacheManager;
     private readonly IUserCacheManager _userCacheManager;
-    private readonly IIdentityClient _identityClient;
-    private readonly IOptionsSnapshot<IdentityConfigs> _identityConfigs;
     private readonly IMapper _mapper;
-    private readonly IOrgUserRepository _orgUserRepository;
+    private readonly IAccountRepository _accountRepository;
+    private readonly IAccountDomainService _accountDomainService;
 
     public UserController(
         ISessionContext sessionContext,
@@ -46,10 +36,9 @@ public class UserController : BaseController
         IUserRepository userRepository,
         ITelCacheManager telCacheManager,
         IUserCacheManager userCacheManager,
-        IIdentityClient identityClient,
-        IOptionsSnapshot<IdentityConfigs> identityConfigs,
         IMapper mapper,
-        IOrgUserRepository orgUserRepository)
+        IAccountRepository accountRepository,
+        IAccountDomainService accountDomainService)
     {
         _sessionContext = sessionContext;
         _userDomainService = userDomainService;
@@ -57,10 +46,9 @@ public class UserController : BaseController
         _userRepository = userRepository;
         _telCacheManager = telCacheManager;
         _userCacheManager = userCacheManager;
-        _identityClient = identityClient;
-        _identityConfigs = identityConfigs;
         _mapper = mapper;
-        _orgUserRepository = orgUserRepository;
+        _accountRepository = accountRepository;
+        _accountDomainService = accountDomainService;
     }
 
     /// <summary>
@@ -116,92 +104,57 @@ public class UserController : BaseController
     /// <summary>
     /// 更新用户
     /// </summary>
-    /// <param name="userDto"></param>
+    /// <param name="dto"></param>
     /// <returns></returns>
     [Permission(EPermission.UpdateUser)]
     [HttpPut]
-    public async Task Update([FromBody] UpdateUserDto userDto)
+    public async Task Update([FromBody] UpdateUserDto dto)
     {
-        var user = await _userRepository.GetAsync(userDto.Id, HttpContext.RequestAborted);
-        if (user is null || user.IsDeleted)
-            throw UserFriendlyException.SameMessage("无效用户编号");
-        if (await IsAccountLock(user.Id))
+        var account = await _accountRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+        if (account is null)
+            throw UserFriendlyException.SameMessage("该账号不存在");
+        if (_accountDomainService.IsLockedOut(account))
             throw UserFriendlyException.SameMessage("该账号已被锁定");
 
-        _mapper.Map(userDto, user);
+        var user = await _userRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+        if (user is null)
+            throw UserFriendlyException.SameMessage("无效用户编号");
+
+        _mapper.Map(dto, user);
         await _userRepository.UpdateAsync(user, HttpContext.RequestAborted);
-        //查询用户组织架构
-        var orgUser = await _orgUserRepository.GetAsync(x => x.UserId == user.Id);
-        if (orgUser is null)
-        {
-            //新增
-            if (!string.IsNullOrEmpty(userDto.OrgId) && !string.IsNullOrEmpty(userDto.OrgCode))
-            {
-                await _orgUserRepository.AddAsync(new OrgUser() { OrgId = userDto.OrgId, OrgCode = userDto.OrgCode, UserId = user.Id });
-            }
-        }
-        else
-        {
-            //修改
-            if (!string.IsNullOrEmpty(userDto.OrgId) && !string.IsNullOrEmpty(userDto.OrgCode))
-            {
-                orgUser.OrgId = orgUser.Id;
-                orgUser.OrgCode = orgUser.OrgCode;
-                await _orgUserRepository.UpdateAsync(orgUser, HttpContext.RequestAborted);
-            }
-            else
-            {
-                //删除
-                await _orgUserRepository.RemoveAsync(orgUser.Id, false, HttpContext.RequestAborted);
-            }
-        }
     }
 
     /// <summary>
     /// 新增用户
     /// </summary>
-    /// <param name="userDto"></param>
+    /// <param name="dto"></param>
     /// <returns></returns>
     [Permission(EPermission.AddUser)]
     [HttpPost]
-    public async Task<string> Add([FromBody] AddUserDto userDto)
+    public async Task<string> Add([FromBody] AddUserDto dto)
     {
-        var getAccountRsp = await _identityClient.GetUserAsync(userDto.UserName, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(getAccountRsp, "GetUserAsync");
-        var account = getAccountRsp.Result;
+        var account = await _accountRepository.GetAsync(d => d.UserName == dto.UserName, HttpContext.RequestAborted);
         if (account is null)
         {
-            var identityConfigs = _identityConfigs.Value;
+            account = _mapper.Map<Account>(dto);
+            await _accountRepository.AddAsync(account, HttpContext.RequestAborted);
+            var user = _mapper.Map<User>(dto);
+            user.Id = account.Id;
+            await _userRepository.AddAsync(user, HttpContext.RequestAborted);
 
-            var addAccountRsp = await _identityClient.AddUserAsync(new IdentityUserDto
-            {
-                ClientId = identityConfigs.ClientId,
-                UserName = userDto.UserName,
-                Email = $"{userDto.UserName}@fw.com",
-                DislayName = userDto.Name ?? userDto.UserName
-            }, HttpContext.RequestAborted);
-            if (addAccountRsp is null || !addAccountRsp.IsSuccess)
-                throw new UserFriendlyException("identity service insert fail: AddUserAsync", "新增用户失败!");
-
-            var user = _mapper.Map<User>(userDto);
-            user.Id = addAccountRsp.Result;
-            string userid = await _userRepository.AddAsync(user, HttpContext.RequestAborted);
-            //如果有组织架构就新增一条数据
-            if (!string.IsNullOrEmpty(userDto.OrgId) && !string.IsNullOrEmpty(userDto.OrgCode))
-            {
-                await _orgUserRepository.AddAsync(new OrgUser() { OrgId = userDto.OrgId, OrgCode = userDto.OrgCode, UserId = userid });
-            }
-            return userid;
+            //initial pwd
+            await _accountDomainService.InitialPasswordAsync(account, HttpContext.RequestAborted);
+            return account.Id;
         }
         else
         {
-            if (await IsAccountLock(account.Id))
+            if (_accountDomainService.IsLockedOut(account))
                 throw UserFriendlyException.SameMessage("该账号已被锁定,请联系管理员");
 
             var user = await _userRepository.GetAsync(account.Id, HttpContext.RequestAborted);
             if (user is null)
             {
-                user = _mapper.Map<User>(userDto);
+                user = _mapper.Map<User>(dto);
                 user.Id = account.Id;
                 return await _userRepository.AddAsync(user, HttpContext.RequestAborted);
             }
@@ -209,6 +162,7 @@ public class UserController : BaseController
             if (user.IsDeleted)
             {
                 user.Recover();
+                _mapper.Map(dto, user);
                 await _userRepository.UpdateAsync(user);
                 return user.Id;
             }
@@ -230,9 +184,10 @@ public class UserController : BaseController
         if (work is not null)
             throw UserFriendlyException.SameMessage("该用户正在工作中,请下班以后再删除");
 
-        var response = await _identityClient.LockUserAsync(new UserLockDto(id), HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(response, "LockUserAsync");
-
+        var account = await _accountRepository.GetAsync(id, HttpContext.RequestAborted);
+        if (account is null)
+            throw UserFriendlyException.SameMessage("该账号不存在");
+        await _accountDomainService.LockOutAsync(account, cancellationToken: HttpContext.RequestAborted);
         await _userRepository.RemoveAsync(id, true, HttpContext.RequestAborted);
     }
 
@@ -240,7 +195,6 @@ public class UserController : BaseController
     /// 查询用户当前状态
     /// </summary>
     /// <returns></returns>
-    //[AllowAnonymous]
     [HttpGet("state")]
     public async Task<UserStateDto> GetUserState()
     {
@@ -259,18 +213,19 @@ public class UserController : BaseController
     }
 
     /// <summary>
-    /// 分页查询用户角色
+    /// 查询用户角色
     /// </summary>
     /// <returns></returns>
     [Permission(EPermission.GetUserRoles)]
-    [HttpGet("roles")]
-    public async Task<PagedDto<IdentityRoleDto>> GetUserRoles([FromQuery] UserRolesPagedDto dto)
+    [HttpGet("{id}/roles")]
+    public async Task<IReadOnlyList<RoleDto>> GetUserRoles(string id)
     {
-        var pageDto = _mapper.Map<PageDto>(dto);
-        var getUserRolesRsp = await _identityClient.GetUserRolesAsync(dto.UserId, pageDto, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(getUserRolesRsp, "GetUserRolesAsync");
-        var result = getUserRolesRsp.Result;
-        return new PagedDto<IdentityRoleDto>(result.TotalCount, result.Roles);
+        var account = await _accountRepository.Queryable()
+            .Includes(d => d.Roles)
+            .FirstAsync(d => d.Id == id);
+        if (account == null)
+            throw UserFriendlyException.SameMessage("无效账号编号");
+        return _mapper.Map<IReadOnlyList<RoleDto>>(account.Roles);
     }
 
     /// <summary>
@@ -281,8 +236,7 @@ public class UserController : BaseController
     [HttpPost("roles")]
     public async Task SetUserRoles([FromBody] SetUserRolesDto dto)
     {
-        var setUserRolesRsp = await _identityClient.SetUserRolesAsync(dto, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(setUserRolesRsp, "SetUserRolesAsync");
+        await _accountRepository.SetAccountRolesAsync(dto.UserId, dto.RoleIds, HttpContext.RequestAborted);
     }
 
     /// <summary>
@@ -307,10 +261,13 @@ public class UserController : BaseController
     [HttpPost("change-pwd")]
     public async Task ChangePassword([FromBody] ChangePasswordDto dto)
     {
-        var changepwdDto = _mapper.Map<UserChangePasswordDto>(dto);
-        changepwdDto.UserId = _sessionContext.RequiredUserId;
-        var changepwdRsp = await _identityClient.ChangePasswordAsync(changepwdDto, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(changepwdRsp, "ChangePasswordAsync");
+        var account = await _accountRepository.GetAsync(_sessionContext.RequiredUserId, HttpContext.RequestAborted);
+        if (account == null)
+            throw UserFriendlyException.SameMessage("无效账号编号");
+        var result = await _accountDomainService.ResetPasswordAsync(account, dto.CurrentPassword, dto.NewPassword,
+             HttpContext.RequestAborted);
+        if (!result.Succeeded)
+            throw new UserFriendlyException(string.Join(',', result.Errors.Select(d => d.Description).ToList()));
     }
 
     /// <summary>
@@ -320,26 +277,17 @@ public class UserController : BaseController
     [HttpPost("initial-pwd/{userId}")]
     public async Task InitialPassword(string userId)
     {
-        var initpwdRsp = await _identityClient.InitialPasswordAsync(userId, HttpContext.RequestAborted);
-        CheckHttpRequestSuccess(initpwdRsp, "InitialPasswordAsync");
+        var account = await _accountRepository.GetAsync(_sessionContext.RequiredUserId, HttpContext.RequestAborted);
+        if (account == null)
+            throw UserFriendlyException.SameMessage("无效账号编号");
+        await _accountDomainService.InitialPasswordAsync(account, HttpContext.RequestAborted);
     }
 
-    #region private
-
-    private async Task<bool> IsAccountLock(string userId)
+    [HttpGet]
+    public async Task<IReadOnlyList<UserDto>> Query([FromQuery]IReadOnlyList<string> ids)
     {
-        var response = await _identityClient.IsAccountLockAsync(userId, HttpContext.RequestAborted);
-        if (response is null || !response.IsSuccess)
-            throw new UserFriendlyException("identity service request fail: IsAccountLockAsync");
-        return response.Result;
+        var users = await _userRepository.Queryable().ToListAsync(d => ids.Contains(d.Id));
+        return _mapper.Map<IReadOnlyList<UserDto>>(users);
     }
 
-    private void CheckHttpRequestSuccess(ApiResponse response, string msg)
-    {
-        if (response == null || !response.IsSuccess)
-            throw new UserFriendlyException($"identity service request failed: {msg}");
-    }
-
-    #endregion
-
 }

+ 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

+ 0 - 1
src/Hotline.Api/Realtimes/CallCenterHub.cs

@@ -29,7 +29,6 @@ public class CallCenterHub : Hub
     public override async Task OnConnectedAsync()
     {
         var userId = _sessionContext.RequiredUserId;
-        //var work = await _workRepository.GetCurrentWorkByUserAsync(userId, Context.ConnectionAborted);
         var work = _userCacheManager.GetWorkByUser(userId);
         if (work == null)
             throw new UserFriendlyException($"未查询到上班记录, userId: {userId}");

+ 41 - 33
src/Hotline.Api/StartupExtensions.cs

@@ -9,9 +9,11 @@ using Hotline.Application.Contracts;
 using Hotline.Application.Contracts.Configurations;
 using Hotline.CacheManager;
 using Hotline.CallCenter.Devices;
+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;
@@ -19,11 +21,14 @@ using MapsterMapper;
 using MediatR;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
 using Microsoft.IdentityModel.Tokens;
 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;
 
@@ -42,13 +47,14 @@ internal static class StartupExtensions
 #endif
 
         services.Configure<DeviceConfigs>(d => configuration.GetSection(nameof(DeviceConfigs)).Bind(d));
-        //services.Configure<WorkTimeSettings>(d => configuration.GetSection(nameof(WorkTimeSettings)).Bind(d));
-
+        services.Configure<WorkTimeSettings>(d => configuration.GetSection(nameof(WorkTimeSettings)).Bind(d));
+        services.Configure<IdentityConfiguration>(d => configuration.GetSection(nameof(IdentityConfiguration)).Bind(d));
 
         // Add services to the container.
         services
             .BatchInjectServices()
             .AddApplication()
+            .AddScoped<IPasswordHasher<Account>, PasswordHasher<Account>>()
             ;
 
         var identityConfigs = configuration.GetSection(nameof(IdentityConfigs)).Get<IdentityConfigs>();
@@ -66,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;
+        }
+    }
+}

+ 34 - 2
src/Hotline.Api/appsettings.Development.json

@@ -28,7 +28,7 @@
       },
       {
         "Name": "Exceptionless"
-      },
+      }
       //{
       //  "Name": "File",
       //  "Args": {
@@ -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;",
@@ -76,5 +76,37 @@
     "ClientId": "hotline_server",
     "ClientSecret": "ce2fae0e-f0f6-46d6-bd79-1f1a31dff494",
     "ClientScope": "identity_admin_api"
+  },
+  "IdentityConfiguration": {
+    "Password": {
+      "RequiredLength": 8,
+      "RequireNonAlphanumeric": true,
+      "RequireLowercase": true,
+      "RequireUppercase": true
+    },
+    "User": {
+      "RequireUniqueEmail": false
+    },
+    "SignIn": {
+      "RequireConfirmedAccount": false
+    },
+    "Lockout": {
+      "MaxFailedAccessAttempts": 5,
+      "DefaultLockoutTimeSpan": "00:10:00"
+    },
+    "Account": {
+      "DefaultPassword": "Fwkj@789"
+    },
+    "Jwt": {
+      "SecretKey": "e660d04ef1d3410798c953f5d7b8a4e1",
+      "Issuer": "hotline_server",
+      "Audience": "hotline",
+      "Scope": "hotline_api",
+      "Expired": 86400 //seceonds
+    }
+  },
+  "DatabaseConfiguration": {
+    "ApplyDbMigrations": false,
+    "ApplySeed": false
   }
 }

+ 4 - 0
src/Hotline.Application.Contracts/Hotline.Application.Contracts.csproj

@@ -14,4 +14,8 @@
     <ProjectReference Include="..\Hotline\Hotline.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Folder Include="Configurations\" />
+  </ItemGroup>
+
 </Project>

+ 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 - 0
src/Hotline.Application/Mappers/MapperConfigs.cs

@@ -1,5 +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,6 +20,8 @@ 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));
         }
     }
 }

+ 20 - 8
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;
@@ -118,23 +119,34 @@ namespace Hotline.Repository.SqlSugar
             return await query.ToListAsync();
         }
 
-        public async Task<bool> AnyAsync(CancellationToken cancellationToken = default) =>
-            await Db.Queryable<TEntity>().AnyAsync();
+        public Task<bool> AnyAsync(CancellationToken cancellationToken = default) => Db.Queryable<TEntity>().AnyAsync();
 
-        public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) =>
-            await Db.Queryable<TEntity>().AnyAsync(predicate);
+        public Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) =>
+             Db.Queryable<TEntity>().AnyAsync(predicate);
 
-        public async Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) =>
-            await Db.Queryable<TEntity>().CountAsync(predicate);
+        public Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) =>
+             Db.Queryable<TEntity>().CountAsync(predicate);
 
-        public ISugarQueryable<TEntity> Queryable(bool permissionVerify = false)
+        public ISugarQueryable<TEntity> Queryable(bool permissionVerify = false, bool includeDeleted = false)
         {
+            if (includeDeleted)
+                Db.QueryFilter.Clear();
+
             var query = Db.Queryable<TEntity>();
             if (permissionVerify)
                 query = query.DataPermissionFiltering(_dataPermissionFilterBuilder);
             return query;
         }
 
+        public UpdateNavTaskInit<TEntity, TEntity> UpdateNav(TEntity entity) => Db.UpdateNav(entity);
+
+        public UpdateNavTaskInit<TEntity, TEntity> UpdateNav(TEntity entity, UpdateNavRootOptions options) => Db.UpdateNav(entity, options);
+
+        public UpdateNavTaskInit<TEntity, TEntity> UpdateNav(List<TEntity> entities) => Db.UpdateNav(entities);
+
+        public UpdateNavTaskInit<TEntity, TEntity> UpdateNav(List<TEntity> entities, UpdateNavRootOptions options) => Db.UpdateNav(entities, options);
+
+
         /// <summary>
         /// 基础分页
         /// </summary>
@@ -156,7 +168,7 @@ namespace Hotline.Repository.SqlSugar
             var query = Db.Queryable<TEntity>().Where(predicate);
             if (permissionVerify)
                 query = query.DataPermissionFiltering(_dataPermissionFilterBuilder);
-            
+
             if (whereIfs.Any())
             {
                 foreach (var whereIf in whereIfs)

+ 7 - 1
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;
@@ -16,6 +17,11 @@ public abstract class BaseRepositoryWorkflow<TEntity> : BaseRepository<TEntity>,
         _dataPermissionFilterBuilder = dataPermissionFilterBuilder;
     }
 
-    public ISugarQueryable<TEntity> Queryable() => Db.Queryable<TEntity>().WorkflowDataFiltering(_dataPermissionFilterBuilder);
+    public ISugarQueryable<TEntity> Queryable(bool includeDeleted = false)
+    {
+        if (includeDeleted)
+            Db.QueryFilter.Clear();
 
+        return Db.Queryable<TEntity>().WorkflowDataFiltering(_dataPermissionFilterBuilder);
+    }
 }

+ 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";
+    }
+}

+ 33 - 0
src/Hotline.Repository.SqlSugar/Identity/AccountRepository.cs

@@ -0,0 +1,33 @@
+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 Hotline.Repository.SqlSugar.DataPermissions;
+using SqlSugar;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Repository.SqlSugar.Identity
+{
+    public class AccountRepository : BaseRepository<Account>, IAccountRepository, IScopeDependency
+    {
+        public AccountRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+        {
+        }
+
+        public async Task SetAccountRolesAsync(string userId, ICollection<string> roleIds, CancellationToken cancellationToken)
+        {
+            var account = await GetAsync(userId, cancellationToken);
+            if (account == null)
+                throw UserFriendlyException.SameMessage("无效账号编号");
+
+            account.Roles = roleIds.Select(d => new Role { Id = d }).ToList();
+            await Db.UpdateNav(account)
+                .Include(d => d.Roles)
+                .ExecuteCommandAsync();
+        }
+    }
+}

+ 19 - 0
src/Hotline.Repository.SqlSugar/Identity/RoleRepository.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Identity.Roles;
+using Hotline.Repository.SqlSugar.DataPermissions;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace Hotline.Repository.SqlSugar.Identity
+{
+    public class RoleRepository : BaseRepository<Role>, IRoleRepository, IScopeDependency
+    {
+        public RoleRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+        {
+        }
+    }
+}

+ 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);
-        }
-    }
-}

+ 0 - 17
src/Hotline.Repository.SqlSugar/User/OrgUserRepository.cs

@@ -1,17 +0,0 @@
-using Hotline.CallCenter.Calls;
-using Hotline.Repository.SqlSugar.DataPermissions;
-using Hotline.Settings;
-using SqlSugar;
-using XF.Domain.Dependency;
-
-namespace Hotline.Repository.SqlSugar.User
-{
-    public class OrgUserRepository : BaseRepository<OrgUser>, IOrgUserRepository, IScopeDependency
-    {
-        public OrgUserRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
-        {
-        }
-
-
-    }
-}

+ 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)
     {

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

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Dtos.Identity
+{
+    public class LoginDto
+    {
+        public string UserName { get; set; }
+        public string Password { get; set; }
+    }
+}

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

@@ -0,0 +1,14 @@
+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; }
+    }
+}

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

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Dtos.Role
+{
+    public class RoleDto : AddRoleDto
+    {
+        public string Id { get; set; }
+        public IReadOnlyList<string> AccountIds { get; set; }
+    }
+
+    public class AddRoleDto
+    {
+        public string Name { get; set; }
+
+        public string ClientId { get; set; }
+
+        public string DisplayName { get; set; }
+
+        public string? Description { get; set; }
+    }
+
+    public class UpdateRoleDto
+    {
+        public string Id { get; set; }
+        
+        public string DisplayName { get; set; }
+
+        public string? Description { get; set; }
+    }
+}

+ 2 - 2
src/Hotline.Share/Dtos/User/ChangePasswordDto.cs

@@ -8,8 +8,8 @@ namespace Hotline.Share.Dtos.User
 {
     public class ChangePasswordDto
     {
-        public string Password { get; set; }
+        public string CurrentPassword { get; set; }
         
-        public string ConfirmPassword { get; set; }
+        public string NewPassword { get; set; }
     }
 }

+ 5 - 2
src/Hotline.Share/Dtos/User/UserRolesPagedDto.cs → src/Hotline.Share/Dtos/User/SetUserRolesDto.cs

@@ -3,9 +3,12 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
-using Hotline.Share.Requests;
 
 namespace Hotline.Share.Dtos.User
 {
-    public record UserRolesPagedDto(string UserId) : PagedRequest;
+    public class SetUserRolesDto
+    {
+        public string UserId { get; set; }
+        public ICollection<string> RoleIds { get; set; }
+    }
 }

+ 5 - 0
src/Hotline.Share/Requests/PagedRequest.cs

@@ -8,4 +8,9 @@ namespace Hotline.Share.Requests
 
         public int Skip() => (PageIndex - 1) * PageSize;
     }
+
+    public record PagedKeywordRequest : PagedRequest
+    {
+        public string? Keyword { get; set; }
+    }
 }

+ 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;
 

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

@@ -0,0 +1,62 @@
+using Hotline.Identity.Roles;
+using SqlSugar;
+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; }
+
+        [SugarColumn(IsNullable = true)]
+        public string? NormalizedUserName { get; set; }
+
+        [SugarColumn(IsNullable = true)]
+        public string? Email { get; set; }
+
+        [SugarColumn(IsNullable = true)]
+        public string? NormalizedEmail { get; set; }
+
+        public bool EmailConfirmed { get; set; }
+
+        [SugarColumn(Length = 2000, IsNullable = true)]
+        public string PasswordHash { get; set; }
+
+        [SugarColumn(Length = 2000, IsNullable = true)]
+        public string SecurityStamp { get; set; }
+
+        [SugarColumn(Length = 2000, IsNullable = true)]
+        public string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();
+
+        [SugarColumn(IsNullable = true)]
+        public string? PhoneNo { get; set; }
+
+        public bool PhoneNoConfirmed { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date and time, in UTC, when any user lockout ends.
+        /// </summary>
+        /// <remarks>
+        /// A value in the past means the user is not locked out.
+        /// </remarks>
+        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; } = true;
+
+        public int AccessFailedCount { get; set; }
+
+        public string ClientId { get; set; }
+
+        public string Name { get; set; }
+
+        public bool PasswordChanged { get; set; }
+
+        [Navigate(typeof(AccountRole), nameof(AccountRole.AccountId), nameof(AccountRole.RoleId))]
+        public List<Role> Roles { get; set; }
+    }
+}

+ 96 - 0
src/Hotline/Identity/Accounts/AccountDomainService.cs

@@ -0,0 +1,96 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Options;
+using XF.Domain.Password;
+
+namespace Hotline.Identity.Accounts;
+
+public class AccountDomainService : IAccountDomainService, IScopeDependency
+{
+    private readonly IPasswordHasher<Account> _passwordHasher;
+    private readonly IAppPasswordValidator _appPasswordValidator;
+    private readonly IAccountRepository _accountRepository;
+    private readonly IOptionsSnapshot<IdentityConfiguration> _identityOptionsAccessor;
+
+    public AccountDomainService(
+        IPasswordHasher<Account> passwordHasher,
+        IAppPasswordValidator appPasswordValidator,
+        IAccountRepository accountRepository,
+        IOptionsSnapshot<IdentityConfiguration> identityOptionsAccessor)
+    {
+        _passwordHasher = passwordHasher;
+        _appPasswordValidator = appPasswordValidator;
+        _accountRepository = accountRepository;
+        _identityOptionsAccessor = identityOptionsAccessor;
+    }
+
+    public async Task<IdentityResult> ResetPasswordAsync(Account account, string password, string newPassword, CancellationToken cancellationToken = default)
+    {
+        if (account == null)
+            throw new ArgumentNullException(nameof(account));
+
+        var verifyResult = VerifyPassword(account, password);
+        if (verifyResult == PasswordVerificationResult.Failed)
+            return IdentityResult.Failed(new IdentityError { Code = nameof(PasswordVerificationResult), Description = "密码校验失败" });
+        var result = await UpdatePasswordHashAsync(account, newPassword, validatePassword: true, cancellationToken);
+        return result;
+    }
+
+    public Task<IdentityResult> InitialPasswordAsync(Account account, CancellationToken cancellationToken = default)
+    {
+        var accountOptions = _identityOptionsAccessor.Value.Account;
+        return UpdatePasswordHashAsync(account, accountOptions.DefaultPassword, true, cancellationToken);
+    }
+
+    public async Task<IdentityResult> UpdatePasswordHashAsync(Account account, string newPassword, bool validatePassword = true, CancellationToken cancellationToken = default)
+    {
+        if (account == null)
+            throw new ArgumentNullException(nameof(account));
+
+        if (validatePassword)
+        {
+            var validate = await _appPasswordValidator.ValidateAsync(newPassword);
+            if (!validate.Succeeded)
+                return validate;
+        }
+        var hash = newPassword != null ? _passwordHasher.HashPassword(account, newPassword) : null;
+        account.PasswordHash = hash;
+        await _accountRepository.UpdateAsync(account, cancellationToken);
+
+        //await UpdateSecurityStampInternal(user);//todo
+        return IdentityResult.Success;
+    }
+
+    public PasswordVerificationResult VerifyPassword(Account account, string pwd)
+    {
+        var hash = account.PasswordHash;
+        if (string.IsNullOrEmpty(hash))
+            return PasswordVerificationResult.Failed;
+        return _passwordHasher.VerifyHashedPassword(account, hash, pwd);
+    }
+
+    public bool IsLockedOut(Account account)
+    {
+        if (account == null)
+            throw new ArgumentNullException(nameof(account));
+        if (!account.LockoutEnabled)
+            return false;
+        if (!account.LockoutEnd.HasValue)
+            return false;
+
+        return account.LockoutEnd >= DateTimeOffset.UtcNow;
+    }
+
+    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 ?? DateTime.MaxValue;
+        await _accountRepository.UpdateAsync(account, cancellationToken);
+    }
+}

+ 13 - 0
src/Hotline/Identity/Accounts/AccountRole.cs

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

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

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Cryptography.KeyDerivation;
+using Microsoft.AspNetCore.Identity;
+
+namespace Hotline.Identity.Accounts
+{
+    public interface IAccountDomainService
+    {
+        //Task<IdentityResult> ResetPasswordAsync(TUser user, string token, string newPassword);
+        Task<IdentityResult> ResetPasswordAsync(Account account, string password, string newPassword, CancellationToken cancellationToken = default);
+        Task<IdentityResult> InitialPasswordAsync(Account account, CancellationToken cancellationToken = default);
+        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, DateTime? lockoutEnd = null, CancellationToken cancellationToken = default);
+    }
+}

+ 14 - 0
src/Hotline/Identity/Accounts/IAccountRepository.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Identity.Accounts
+{
+    public interface IAccountRepository : IRepository<Account>
+    {
+        Task SetAccountRolesAsync(string userId, ICollection<string> roleIds, CancellationToken cancellationToken);
+    }
+}

+ 13 - 0
src/Hotline/Identity/Roles/IRoleRepository.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Identity.Roles
+{
+    public interface IRoleRepository : IRepository<Role>
+    {
+    }
+}

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

@@ -0,0 +1,23 @@
+using Hotline.Identity.Accounts;
+using SqlSugar;
+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; }
+
+        public string ClientId { get; set; }
+
+        public string DisplayName { get; set; }
+
+        [SugarColumn(IsNullable = true)]
+        public string? Description { get; set; }
+
+
+        [Navigate(typeof(AccountRole), nameof(AccountRole.RoleId), nameof(AccountRole.AccountId))]
+        public List<Account> Accounts { 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",
+                }
+            };
+    }
+}

+ 0 - 8
src/Hotline/Settings/IOrgUserRepository.cs

@@ -1,8 +0,0 @@
-using XF.Domain.Repository;
-
-namespace Hotline.Settings
-{
-    public interface IOrgUserRepository : IRepository<OrgUser>
-    {
-    }
-}

+ 0 - 25
src/Hotline/Settings/OrgUser.cs

@@ -1,25 +0,0 @@
-using System.ComponentModel;
-using XF.Domain.Entities;
-using XF.Domain.Repository;
-
-namespace Hotline.Settings
-{
-    [Description("用户组织架构关联")]
-    public class OrgUser: CreationEntity
-    {
-        /// <summary>
-        /// 用户ID
-        /// </summary>
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// 组织架构ID
-        /// </summary>
-        public string OrgId { get; set; }
-
-        /// <summary>
-        /// 组织架构Code
-        /// </summary>
-        public string OrgCode { get; set; }
-    }
-}

+ 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; }
+}

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

@@ -1,4 +1,5 @@
 using System.ComponentModel;
+using Hotline.Settings;
 using SqlSugar;
 using XF.Domain.Entities;
 using XF.Domain.Repository;
@@ -28,10 +29,29 @@ namespace Hotline.Users
         [SugarColumn(IsNullable = true)]
         public string? StaffNo { get; set; }
 
+        /// <summary>
+        /// 部门Id
+        /// </summary>
+        [SugarColumn(IsNullable = true)]
+        public string? OrgId { get; set; }
+
+        /// <summary>
+        /// 部门编码(冗余)
+        /// </summary>
+        [SugarColumn(IsNullable = true)]
+        public string? OrgCode { get; set; }
+
         /// <summary>
         /// 默认分机号
         /// </summary>
         [SugarColumn(IsNullable = true)]
         public string? DefaultTelNo { get; set; }
+
+        /// <summary>
+        /// 所属部门
+        /// </summary>
+
+        [Navigate(NavigateType.OneToOne, nameof(OrgId))]
+        public SystemOrganize Organization { 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();
 

+ 7 - 2
src/XF.Domain.Repository/IRepositorySqlSugar.cs

@@ -12,8 +12,6 @@ namespace XF.Domain.Repository
     public interface IRepositorySqlSugar<TEntity, TKey> : IRepositoryWithTKey<TEntity, string>
         where TEntity : class, IEntity<string>, new()
     {
-        ISugarQueryable<TEntity> Queryable(bool permissionVerify = false);
-
         Task<(int Total, List<TEntity> Items)> QueryPagedAsync(
             Expression<Func<TEntity, bool>> predicate,
             Func<ISugarQueryable<TEntity>, ISugarQueryable<TEntity>> orderByCreator,
@@ -53,5 +51,12 @@ namespace XF.Domain.Repository
             bool permissionVerify = false);
 
         Task UpdateAsync(TEntity entity, bool ignoreNullColumns = true, CancellationToken cancellationToken = default);
+
+        ISugarQueryable<TEntity> Queryable(bool permissionVerify = false, bool includeDeleted = false);
+        
+        UpdateNavTaskInit<TEntity, TEntity> UpdateNav(TEntity entity);
+        UpdateNavTaskInit<TEntity, TEntity> UpdateNav(TEntity entity, UpdateNavRootOptions options);
+        UpdateNavTaskInit<TEntity, TEntity> UpdateNav(List<TEntity> entities);
+        UpdateNavTaskInit<TEntity, TEntity> UpdateNav(List<TEntity> entities, UpdateNavRootOptions options);
     }
 }

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

@@ -11,7 +11,7 @@ namespace XF.Domain.Repository
 {
     public interface IRepositoryWorkflow<TEntity, TKey> where TEntity : class, IEntity<string>, new()
     {
-        ISugarQueryable<TEntity> Queryable();
+        ISugarQueryable<TEntity> Queryable(bool includeDeleted = false);
     }
 
     public interface IRepositoryWorkflow<TEntity> : IRepositoryWorkflow<TEntity, string> where TEntity : class, IEntity<string>, 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; }
+    }
+}

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

@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using XF.Domain.Dependency;
+using XF.Domain.Options;
+
+namespace XF.Domain.Password
+{
+    public interface IAppPasswordValidator
+    {
+        Task<IdentityResult> ValidateAsync(string password);
+    }
+
+    public class AppPasswordValidator : IAppPasswordValidator, IScopeDependency
+    {
+        private readonly PasswordOptions _passwordOptions;
+
+        public AppPasswordValidator(
+            IOptions<IdentityConfiguration>? passwordOptionAccessor = null,
+            IdentityErrorDescriber? errors = null)
+        {
+            _passwordOptions = passwordOptionAccessor?.Value.Password ?? new PasswordOptions();
+            Describer = errors ?? new IdentityErrorDescriber();
+        }
+
+        public IdentityErrorDescriber Describer { get; set; }
+
+        public virtual Task<IdentityResult> ValidateAsync(string password)
+        {
+            if (password == null)
+            {
+                throw new ArgumentNullException(nameof(password));
+            }
+            var errors = new List<IdentityError>();
+            var options = _passwordOptions;
+            if (string.IsNullOrWhiteSpace(password) || password.Length < options.RequiredLength)
+            {
+                errors.Add(Describer.PasswordTooShort(options.RequiredLength));
+            }
+            if (options.RequireNonAlphanumeric && password.All(IsLetterOrDigit))
+            {
+                errors.Add(Describer.PasswordRequiresNonAlphanumeric());
+            }
+            if (options.RequireDigit && !password.Any(IsDigit))
+            {
+                errors.Add(Describer.PasswordRequiresDigit());
+            }
+            if (options.RequireLowercase && !password.Any(IsLower))
+            {
+                errors.Add(Describer.PasswordRequiresLower());
+            }
+            if (options.RequireUppercase && !password.Any(IsUpper))
+            {
+                errors.Add(Describer.PasswordRequiresUpper());
+            }
+            if (options.RequiredUniqueChars >= 1 && password.Distinct().Count() < options.RequiredUniqueChars)
+            {
+                errors.Add(Describer.PasswordRequiresUniqueChars(options.RequiredUniqueChars));
+            }
+            return
+                Task.FromResult(errors.Count == 0
+                    ? IdentityResult.Success
+                    : IdentityResult.Failed(errors.ToArray()));
+        }
+
+        /// <summary>
+        /// Returns a flag indicating whether the supplied character is a digit.
+        /// </summary>
+        /// <param name="c">The character to check if it is a digit.</param>
+        /// <returns>True if the character is a digit, otherwise false.</returns>
+        public virtual bool IsDigit(char c)
+        {
+            return c >= '0' && c <= '9';
+        }
+
+        /// <summary>
+        /// Returns a flag indicating whether the supplied character is a lower case ASCII letter.
+        /// </summary>
+        /// <param name="c">The character to check if it is a lower case ASCII letter.</param>
+        /// <returns>True if the character is a lower case ASCII letter, otherwise false.</returns>
+        public virtual bool IsLower(char c)
+        {
+            return c >= 'a' && c <= 'z';
+        }
+
+        /// <summary>
+        /// Returns a flag indicating whether the supplied character is an upper case ASCII letter.
+        /// </summary>
+        /// <param name="c">The character to check if it is an upper case ASCII letter.</param>
+        /// <returns>True if the character is an upper case ASCII letter, otherwise false.</returns>
+        public virtual bool IsUpper(char c)
+        {
+            return c >= 'A' && c <= 'Z';
+        }
+
+        /// <summary>
+        /// Returns a flag indicating whether the supplied character is an ASCII letter or digit.
+        /// </summary>
+        /// <param name="c">The character to check if it is an ASCII letter or digit.</param>
+        /// <returns>True if the character is an ASCII letter or digit, otherwise false.</returns>
+        public virtual bool IsLetterOrDigit(char c)
+        {
+            return IsUpper(c) || IsLower(c) || IsDigit(c);
+        }
+    }
+}

+ 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>