xf vor 2 Jahren
Ursprung
Commit
5fded5036b
35 geänderte Dateien mit 735 neuen und 252 gelöschten Zeilen
  1. 1 1
      src/Hotline.Api/Controllers/HomeController.cs
  2. 44 33
      src/Hotline.Api/Controllers/RoleController.cs
  3. 41 30
      src/Hotline.Api/Controllers/TestController.cs
  4. 59 118
      src/Hotline.Api/Controllers/UserController.cs
  5. 0 1
      src/Hotline.Api/Realtimes/CallCenterHub.cs
  6. 5 1
      src/Hotline.Api/StartupExtensions.cs
  7. 22 1
      src/Hotline.Api/appsettings.Development.json
  8. 4 0
      src/Hotline.Application.Contracts/Hotline.Application.Contracts.csproj
  9. 2 1
      src/Hotline.Application/Mappers/MapperConfigs.cs
  10. 19 8
      src/Hotline.Repository.SqlSugar/BaseRepository.cs
  11. 6 1
      src/Hotline.Repository.SqlSugar/BaseRepositoryWorkflow.cs
  12. 33 0
      src/Hotline.Repository.SqlSugar/Identity/AccountRepository.cs
  13. 19 0
      src/Hotline.Repository.SqlSugar/Identity/RoleRepository.cs
  14. 0 17
      src/Hotline.Repository.SqlSugar/User/OrgUserRepository.cs
  15. 12 0
      src/Hotline.Share/Dtos/Identity/LoginDto.cs
  16. 14 0
      src/Hotline.Share/Dtos/Role/QueryRolesPagedDto.cs
  17. 33 0
      src/Hotline.Share/Dtos/Role/RoleDto.cs
  18. 2 2
      src/Hotline.Share/Dtos/User/ChangePasswordDto.cs
  19. 5 2
      src/Hotline.Share/Dtos/User/SetUserRolesDto.cs
  20. 5 0
      src/Hotline.Share/Requests/PagedRequest.cs
  21. 61 0
      src/Hotline/Identity/Accounts/Account.cs
  22. 95 0
      src/Hotline/Identity/Accounts/AccountDomainService.cs
  23. 12 0
      src/Hotline/Identity/Accounts/AccountRole.cs
  24. 22 0
      src/Hotline/Identity/Accounts/IAccountDomainService.cs
  25. 14 0
      src/Hotline/Identity/Accounts/IAccountRepository.cs
  26. 14 0
      src/Hotline/Identity/IIdentityDomainService.cs
  27. 13 0
      src/Hotline/Identity/Roles/IRoleRepository.cs
  28. 22 0
      src/Hotline/Identity/Roles/Role.cs
  29. 0 8
      src/Hotline/Settings/IOrgUserRepository.cs
  30. 0 25
      src/Hotline/Settings/OrgUser.cs
  31. 20 0
      src/Hotline/Users/User.cs
  32. 7 2
      src/XF.Domain.Repository/IRepositorySqlSugar.cs
  33. 1 1
      src/XF.Domain.Repository/IRepositoryWorkflow.cs
  34. 109 0
      src/XF.Domain/Password/IAppPasswordValidator.cs
  35. 19 0
      src/XF.Domain/Password/IdentityConfiguration.cs

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

+ 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;
 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()
+    {
+        
+    }
 
-        /// <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: 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>
     /// 

+ 59 - 118
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;
-
-            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", "新增用户失败!");
+            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 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,10 @@ 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)
-    {
-        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;
-    }
-
-    private void CheckHttpRequestSuccess(ApiResponse response, string msg)
-    {
-        if (response == null || !response.IsSuccess)
-            throw new UserFriendlyException($"identity service request failed: {msg}");
-    }
-
-    #endregion
-
 }

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

+ 5 - 1
src/Hotline.Api/StartupExtensions.cs

@@ -9,6 +9,7 @@ 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;
@@ -19,11 +20,13 @@ 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.Password;
 
 namespace Hotline.Api;
 
@@ -43,12 +46,13 @@ internal static class StartupExtensions
 
         services.Configure<DeviceConfigs>(d => configuration.GetSection(nameof(DeviceConfigs)).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>();

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

@@ -28,7 +28,7 @@
       },
       {
         "Name": "Exceptionless"
-      },
+      }
       //{
       //  "Name": "File",
       //  "Args": {
@@ -76,5 +76,26 @@
     "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"
+    }
   }
 }

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

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

@@ -1,4 +1,5 @@
 using Hotline.CallCenter.BlackLists;
+using Hotline.Identity.Accounts;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.User;
 using Hotline.Users;
@@ -16,7 +17,7 @@ namespace Hotline.Application.Mappers
 
             config.NewConfig<AddUserDto, User>()
                 .Map(d => d.Name, x => x.Name ?? x.UserName);
-
+            
         }
     }
 }

+ 19 - 8
src/Hotline.Repository.SqlSugar/BaseRepository.cs

@@ -118,23 +118,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 +167,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)

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

@@ -16,6 +16,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);
+    }
 }

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

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

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Dtos.Identity
+{
+    public class LoginDto
+    {
+    }
+}

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

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

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

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

@@ -0,0 +1,61 @@
+using Hotline.Identity.Roles;
+using SqlSugar;
+using XF.Domain.Repository;
+
+namespace Hotline.Identity.Accounts
+{
+    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 DateTimeOffset? LockoutEnd { get; set; }
+
+        /// <summary>
+        /// Gets or sets a flag indicating if the user could be locked out.
+        /// </summary>
+        /// <value>True if the user could be locked out, otherwise false.</value>
+        public bool LockoutEnabled { get; set; }
+
+        public 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; }
+    }
+}

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

@@ -0,0 +1,95 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+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, DateTimeOffset? lockoutEnd = null, CancellationToken cancellationToken = default)
+    {
+        if (account == null)
+            throw new ArgumentNullException(nameof(account));
+        if (!account.LockoutEnabled)
+            throw UserFriendlyException.SameMessage("该账号不允许锁定");
+
+        account.LockoutEnd = lockoutEnd ?? DateTimeOffset.MaxValue;
+        await _accountRepository.UpdateAsync(account, cancellationToken);
+    }
+}

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

@@ -0,0 +1,12 @@
+using SqlSugar;
+
+namespace Hotline.Identity.Accounts;
+
+public class AccountRole
+{
+    [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, DateTimeOffset? 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);
+    }
+}

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

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

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

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

@@ -0,0 +1,22 @@
+using Hotline.Identity.Accounts;
+using SqlSugar;
+using XF.Domain.Repository;
+
+namespace Hotline.Identity.Roles
+{
+    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 - 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; }
-    }
-}

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

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

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

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

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

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