【ABP】项目示例(5)——应用服务

应用服务

在上一章节中,已经完成了领域服务的设计,在这一章节中,实现应用服务,主要内容为业务逻辑的设计和组合
创建名称为General.Backend.Application.Contracts的标准类库,以及在该类库中新建名称为Dtos的文件夹
在程序包管理控制台选中General.Backend.Application.Contracts,执行以下命令安装ABP应用服务依赖相关的Nuget包

Install-Package Volo.Abp.Ddd.Application.Contracts -v 8.3.0

General.Backend.Application.Contracts添加项目引用DPower.Store.Domain.Shared
新建名称为GeneralApplicationContractsModule的应用服务依赖模块类

public class GeneralApplicationContractsModule : AbpModule
{

}

数据传输类

在Dtos文件夹中新建名称为PageDto和PageAndSortDto的分页和排序类

  • PageDto
/// <summary>
/// 分页
/// </summary>
public class PageDto
{
    /// <summary>
    /// 分页大小
    /// </summary>
    public int PageSize { get; set; } = 10;

    /// <summary>
    /// 分页下标
    /// </summary>
    public int PageIndex { get; set; } = 1;
}
  • PageAndSortDto
/// <summary>
/// 排序
/// </summary>
public class PageAndSortDto : PageDto
{
    /// <summary>
    /// 排序类型:asc desc
    /// </summary>
    public string SortType { get; set; } = string.Empty;

    /// <summary>
    /// 排序字段
    /// </summary>
    public string SortField { get; set; } = string.Empty;
}

在Dtos文件夹中分别新建名称为Users、Roles、Menus和Auths的文件夹,用于存放用户、角色、菜单和认证授权相关的数据传输类
在Users文件夹中分别新建名称为UserDto、UserCreateDto、UserUpdateDto、UserQueryDto和UserRoleDto的数据传输类

  • UserDto
/// <summary>
/// 用户
/// </summary>
public class UserDto : AuditedEntityDto<Guid>
{
    /// <summary>
    /// 账号
    /// </summary>
    public string Account { get; set; } = string.Empty;

    /// <summary>
    /// 密码
    /// </summary>
    public string Password { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 联系方式
    /// </summary>
    public string? Contact { get; set; }

    /// <summary>
    /// 地址
    /// </summary>
    public string? Address { get; set; }

    /// <summary>
    /// 是否冻结(0:未冻结,1:已冻结)
    /// </summary>
    public FrozenStatus IsFrozen { get; set; }

    /// <summary>
    /// 角色Id
    /// </summary>
    public List<Guid> RoleIds { get; set; } = [];

    /// <summary>
    /// 角色名称
    /// </summary>
    public List<string> RoleNames { get; set; } = [];
}
  • UserCreateDto
/// <summary>
/// 用户创建
/// </summary>
public class UserCreateDto
{
    /// <summary>
    /// 账号
    /// </summary>
    public string Account { get; set; } = string.Empty;

    /// <summary>
    /// 密码
    /// </summary>
    public string Password { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 联系方式
    /// </summary>
    public string? Contact { get; set; }

    /// <summary>
    /// 地址
    /// </summary>
    public string? Address { get; set; }

    /// <summary>
    /// 用户关联角色列表
    /// </summary>
    public List<Guid> RoleIds { get; set; } = [];
}
  • UserUpdateDto
/// <summary>
/// 用户更新
/// </summary>
public class UserUpdateDto : EntityDto<Guid>
{
    /// <summary>
    /// 密码
    /// </summary>
    public string Password { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 联系方式
    /// </summary>
    public string? Contact { get; set; }

    /// <summary>
    /// 地址
    /// </summary>
    public string? Address { get; set; }

    /// <summary>
    /// 用户关联角色列表
    /// </summary>
    public List<Guid> RoleIds { get; set; } = [];
}
  • UserQueryDto
/// <summary>
/// 用户查询
/// </summary>
public class UserQueryDto : PageAndSortDto
{
    /// <summary>
    /// 用户账号
    /// </summary>
    public string? Account { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string? Name { get; set; } = string.Empty;
}
  • UserRoleDto
/// <summary>
/// 用户角色
/// </summary>
public class UserRoleDto : EntityDto<Guid>, IHasCreationTime
{
    /// <summary>
    /// 用户Id
    /// </summary>
    public Guid UserId { get; set; }

    /// <summary>
    /// 角色Id
    /// </summary>
    public Guid RoleId { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime CreationTime { get; set; }
}

在Roles文件夹中分别新建名称为RoleDto、RoleCreateDto、RoleUpdateDto、RoleQueryDto和RoleMenuDto的数据传输类

  • RoleDto
/// <summary>
/// 角色
/// </summary>
public class RoleDto : AuditedEntityDto<Guid>
{
    /// <summary>
    /// 编码
    /// </summary>
    public string Code { get; set; } = string.Empty;

    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 描述
    /// </summary>
    public string? Remark { get; set; }
}
  • RoleCreateDto
/// <summary>
/// 角色创建
/// </summary>
public class RoleCreateDto
{
    /// <summary>
    /// 编码
    /// </summary>
    public string Code { get; set; } = string.Empty;

    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 描述
    /// </summary>
    public string? Remark { get; set; }
}
  • RoleUpdateDto
/// <summary>
/// 角色更新
/// </summary>
public class RoleUpdateDto : EntityDto<Guid>
{
    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 描述
    /// </summary>
    public string? Remark { get; set; }
}
  • RoleQueryDto
/// <summary>
/// 角色查询
/// </summary>
public class RoleQueryDto : PageAndSortDto
{
    /// <summary>
    /// 角色名称
    /// </summary>
    public string? Name { get; set; } = string.Empty;
}
  • RoleMenuDto
/// <summary>
/// 角色菜单
/// </summary>
public class RoleMenuDto : EntityDto<Guid>, IHasCreationTime
{
    /// <summary>
    /// 角色编码
    /// </summary>
    public string RoleCode { get; set; } = string.Empty;

    /// <summary>
    /// 菜单编码
    /// </summary>
    public string MenuCode { get; set; } = string.Empty;

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime CreationTime { get; }
}

在Menus文件夹中分别新建名称为MenuDto、MenuModuleDto、PermissionDto和PermissionUpdateDto的数据传输类

  • MenuDto
/// <summary>
/// 菜单
/// </summary>
public class MenuDto : EntityDto<Guid>, IHasCreationTime
{
    /// <summary>
    /// 编码
    /// </summary>
    public string Code { get; set; } = string.Empty;

    /// <summary>
    /// 父编码
    /// </summary>
    public string ParentCode { get; set; } = string.Empty;

    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 类型
    /// </summary>
    public string Type { get; set; } = string.Empty;

    /// <summary>
    /// 层级
    /// </summary>
    public int Level { get; set; }

    /// <summary>
    /// 图标
    /// </summary>
    public string Icon { get; set; } = string.Empty;

    /// <summary>
    /// 路由地址
    /// </summary>
    public string UrlAddress { get; set; } = string.Empty;

    /// <summary>
    /// 组件地址
    /// </summary>
    public string ComponentAddress { get; set; } = string.Empty;

    /// <summary>
    /// 排序
    /// </summary>
    public int Sort { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime CreationTime { get; }
}
  • MenuModuleDto
/// <summary>
/// 菜单模块
/// </summary>
public class MenuModuleDto
{
    /// <summary>
    /// 父编码
    /// </summary>
    public string ParentCode { get; set; } = string.Empty;

    /// <summary>
    /// 编码
    /// </summary>
    public string Code { get; set; } = string.Empty;

    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 类型
    /// </summary>
    public string Type { get; set; } = string.Empty;
}
  • PermissionDto
/// <summary>
/// 权限
/// </summary>
public class PermissionDto
{
    /// <summary>
    /// 菜单树
    /// </summary>
    public List<MenuModuleDto> MenuModule { get; set; } = [];

    /// <summary>
    /// 选中的菜单
    /// </summary>
    public List<string> CheckedMenu { get; set; } = [];

    /// <summary>
    /// 选中的模块
    /// </summary>
    public List<string> CheckedModule { get; set; } = [];
}
  • PermissionUpdateDto
/// <summary>
/// 权限更新
/// </summary>
public class PermissionUpdateDto
{
    /// <summary>
    /// 角色Id
    /// </summary>
    public Guid RoleId { get; set; }

    /// <summary>
    /// 菜单模块列表
    /// </summary>
    public List<string> MenuModules { get; set; } = new List<string>();
}

在Auths文件夹中分别新建名称为CaptchaDto、UserLoginDto、LoginUserDto、UserPermissionDto、UserTokenDto、AccessTokenDto、MenuRouterDto、RefreshTokenCreateDto和RefreshTokenDto的数据传输类

  • CaptchaDto
/// <summary>
/// 验证码
/// </summary>
public class CaptchaDto
{
    /// <summary>
    /// 键
    /// </summary>
    public string Key { get; set; } = string.Empty;

    /// <summary>
    /// 验证码文字
    /// </summary>
    public string Text { get; set; } = string.Empty;

    /// <summary>
    /// 验证码图片
    /// </summary>
    public string Img { get; set; } = string.Empty;
}
  • UserLoginDto
/// <summary>
/// 用户登录
/// </summary>
public class UserLoginDto
{
    /// <summary>
    /// 用户账号
    /// </summary>
    public string UserAccount { get; set; } = string.Empty;

    /// <summary>
    /// 用户密码
    /// </summary>
    public string Password { get; set; } = string.Empty;

    /// <summary>
    /// 验证码
    /// </summary>
    public string Captcha { get; set; } = string.Empty;

    /// <summary>
    /// 验证码发放时间时间戳
    /// </summary>
    public string CaptchaTime { get; set; } = string.Empty;
}
  • LoginUserDto
/// <summary>
/// 登录用户
/// </summary>
public class LoginUserDto
{
    /// <summary>
    /// 用户Id
    /// </summary>
    public Guid UserId { get; set; }

    /// <summary>
    /// 用户账号
    /// </summary>
    public string UserAccount { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string UserName { get; set; } = string.Empty;

    /// <summary>
    /// 角色
    /// </summary>
    public string Role { get; set; } = string.Empty;

    /// <summary>
    /// 角色名称
    /// </summary>
    public string RoleName { get; set; } = string.Empty;

    /// <summary>
    /// 访问凭证值
    /// </summary>
    public string AccessToken { get; set; } = string.Empty;

    /// <summary>
    /// 访问凭证有效时间,单位:分钟
    /// </summary>
    public int ExpiresIn { get; set; }

    /// <summary>
    /// 访问过期时间
    /// </summary>
    public DateTime ExpiresTime { get; set; }

    /// <summary>
    /// 刷新凭证值
    /// </summary>
    public string RefreshToken { get; set; } = string.Empty;

    /// <summary>
    /// 刷新凭证有效时间,单位:分钟
    /// </summary>
    public int RefreshExpiresIn { get; set; }

    /// <summary>
    /// 刷新过期时间
    /// </summary>
    public DateTime RefreshExpiresTime { get; set; }
}
  • UserPermissionDto
/// <summary>
/// 用户权限
/// </summary>
public class UserPermissionDto
{
    /// <summary>
    /// 用户Id
    /// </summary>
    public Guid UserId { get; set; }

    /// <summary>
    /// 用户账号
    /// </summary>
    public string UserAccount { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string UserName { get; set; } = string.Empty;

    /// <summary>
    /// 角色
    /// </summary>
    public string Role { get; set; } = string.Empty;

    /// <summary>
    /// 角色名称
    /// </summary>
    public string RoleName { get; set; } = string.Empty;

    /// <summary>
    /// 授权访问菜单
    /// </summary>
    public List<MenuRouterDto> MenuRouters { get; set; } = [];

    /// <summary>
    /// 授权使用功能
    /// </summary>
    public List<string> Modules { get; set; } = [];
}
  • UserTokenDto
/// <summary>
/// 用户令牌
/// </summary>
public class UserTokenDto
{
    /// <summary>
    /// 用户Id
    /// </summary>
    public Guid UserId { get; set; }

    /// <summary>
    /// 用户账号
    /// </summary>
    public string UserAccount { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string UserName { get; set; } = string.Empty;

    /// <summary>
    /// 角色编码
    /// </summary>
    public string RoleCode { get; set; } = string.Empty;

    /// <summary>
    /// 角色名称
    /// </summary>
    public string RoleName { get; set; } = string.Empty;
}
  • AccessTokenDto
/// <summary>
/// 令牌
/// </summary>
public class AccessTokenDto
{
    /// <summary>
    /// 访问令牌
    /// </summary>
    public string AccessToken { get; set; } = string.Empty;

    /// <summary>
    /// 访问过期时间
    /// </summary>
    public DateTime ExpiresTime { get; set; }

    /// <summary>
    /// 访问凭证有效时间,单位:分钟
    /// </summary>
    public int ExpiresIn { get; set; }
}
  • MenuRouterDto
/// <summary>
/// 菜单路由
/// </summary>
public class MenuRouterDto
{
    /// <summary>
    /// 路由名称
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// 路由地址
    /// </summary>
    public string? Path { get; set; }

    /// <summary>
    /// 是否隐藏路由,当设置true的时候该路由不会在侧边栏出现
    /// </summary>
    public bool Hidden { get; set; }

    /// <summary>
    /// 组件地址
    /// </summary>
    public string Component { get; set; } = string.Empty;

    /// <summary>
    /// 重定向地址,当设置noRedirect的时候该路由在面包屑导航中不可被点击
    /// </summary>
    public string Redirect { get; set; } = string.Empty;

    /// <summary>
    /// 当你一个路由下面的children声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
    /// </summary>
    public bool AlwaysShow { get; set; }

    /// <summary>
    /// 菜单内容
    /// </summary>
    public MetaDto Meta { get; set; }

    /// <summary>
    /// 子菜单路由
    /// </summary>
    public List<MenuRouterDto> Children { get; set; } = [];
}

/// <summary>
/// 菜单内容
/// </summary>
public class MetaDto
{
    /// <summary>
    /// 路由在侧边栏和面包屑中展示的名称
    /// </summary>
    public string Title { get; set; } = string.Empty;

    /// <summary>
    /// 路由的图标
    /// </summary>
    public string? Icon { get; set; }
}
  • RefreshTokenCreateDto
/// <summary>
/// 刷新令牌创建
/// </summary>
public class RefreshTokenCreateDto
{
    /// <summary>
    /// 刷新令牌
    /// </summary>
    public string RefreshToken { get; set; } = string.Empty;
}
  • RefreshTokenDto
/// <summary>
/// 令牌
/// </summary>
public class RefreshTokenDto
{
    /// <summary>
    /// 刷新令牌
    /// </summary>
    public string RefreshToken { get; set; } = string.Empty;

    /// <summary>
    /// 访问过期时间
    /// </summary>
    public DateTime RefreshExpiresTime { get; set; }

    /// <summary>
    /// 访问凭证有效时间,单位:分钟
    /// </summary>
    public int RefreshExpiresIn { get; set; }
}

应用服务接口

新建名称为IUserAppService的用户应用服务接口

/// <summary>
/// 用户应用服务
/// </summary>
public interface IUserAppService : IApplicationService
{
    /// <summary>
    /// 获取用户分页列表
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<PagedResultDto<UserDto>> GetListAsync(UserQueryDto input);

    /// <summary>
    /// 新增用户
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<UserDto> CreateAsync(UserCreateDto input);

    /// <summary>
    /// 更新用户
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<UserDto> UpdateAsync(UserUpdateDto input);

    /// <summary>
    /// 获取用户
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public Task<UserDto> GetAsync(Guid id);

    /// <summary>
    /// 删除用户
    /// </summary>
    /// <param name="ids"></param>
    /// <returns></returns>
    public Task DeleteAsync(List<Guid> ids);

    /// <summary>
    /// 解冻用户
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public Task<UserDto> UnfreezeAsync(Guid id);
}

新建名称为IRoleAppService的角色应用服务接口

/// <summary>
/// 角色应用服务
/// </summary>
public interface IRoleAppService : IApplicationService
{
    /// <summary>
    /// 获取角色分页列表
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<PagedResultDto<RoleDto>> GetListAsync(RoleQueryDto input);

    /// <summary>
    /// 新增角色
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<RoleDto> CreateAsync(RoleCreateDto input);

    /// <summary>
    /// 更新角色
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<RoleDto> UpdateAsync(RoleUpdateDto input);

    /// <summary>
    /// 获取角色
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public Task<RoleDto> GetAsync(Guid id);

    /// <summary>
    /// 删除角色
    /// </summary>
    /// <param name="ids"></param>
    /// <returns></returns>
    public Task DeleteAsync(List<Guid> ids);

    /// <summary>
    /// 获取所有角色
    /// </summary>
    /// <returns></returns>
    public Task<List<RoleMapDto>> GetAllRoleAsync();
}

新建名称为IMenuAppService的菜单应用服务接口

/// <summary>
/// 菜单应用服务
/// </summary>
public interface IMenuAppService : IApplicationService
{
    /// <summary>
    /// 根据角色编码获取权限
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public Task<PermissionDto> GetPermissionAsync(Guid id);

    /// <summary>
    /// 更新权限
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task UpdateAsync(PermissionUpdateDto input);
}

新建名称为IAuthAppService 的认证应用服务接口

/// <summary>
/// 认证应用服务
/// </summary>
public interface IAuthAppService : IApplicationService
{
    /// <summary>
    /// 登录
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<LoginUserDto> LoginAsync(UserLoginDto input);

    /// <summary>
    /// 获取当前用户
    /// </summary>
    /// <returns></returns>
    public Task<UserPermissionDto> GetUserAsync();

    /// <summary>
    /// 获取验证码
    /// </summary>
    /// <returns></returns>
    public Task<CaptchaDto> GetCaptchaAsync();

    /// <summary>
    /// 刷新令牌
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public Task<LoginUserDto> RefreshAsync(RefreshTokenCreateDto input);
}

应用服务实现

创建名称为General.Backend.Application的标准类库,新建名称为Options的文件夹,用于存放选项类
在程序包管理控制台选中General.Backend.Application,执行以下命令安装ABP应用服务和对象映射相关的Nuget包

Install-Package Volo.Abp.Ddd.Application -v 8.3.0
Install-Package Volo.Abp.AutoMapper -v 8.3.0

General.Backend.Application添加项目引用General.Backend.Application.Contracts和DPower.Store.Domain
新建名称为GeneralApplicationAutoMapperProfile的对象映射配置类

/// <summary>
/// Profile
/// </summary>
public class GeneralApplicationAutoMapperProfile : Profile
{
    /// <summary>
    /// 
    /// </summary>
    public GeneralApplicationAutoMapperProfile()
    {
        CreateMap<User, UserDto>();

        CreateMap<Role, RoleDto>();
        CreateMap<Role, RoleMapDto>();

        CreateMap<Menu, MenuDto>();
        CreateMap<UserPermissionCacheItem, UserPermissionDto>();
        CreateMap<MenuRouterCacheItem, MenuRouterDto>();
        CreateMap<Meta, MetaDto>();
    }
}

新建名称为GeneralApplicationModule的应用服务模块类

[DependsOn(
    typeof(AbpAutoMapperModule),
    typeof(GeneralDomainModule),
    typeof(GeneralApplicationContractsModule)
    )]
public class GeneralApplicationModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpAutoMapperOptions>(options =>
        {
            options.AddMaps<GeneralApplicationModule>();
        });
    }
}

在程序包管理控制台选中General.Backend.Application,执行以下命令安装MVC和JWT相关的Nuget包

Install-Package Volo.Abp.AspNetCore.Mvc -v 8.3.0
Install-Package System.IdentityModel.Tokens.Jwt -v 7.1.2

在名称为Options文件夹下,新建名称为JwtOptions的JWT配置选项类,后续在启动项目中的默认加载文件appsetting.json中配置具体的参数

/// <summary>
/// JsonWebToken
/// </summary>
public class JwtOptions
{
    /// <summary>
    /// JWT
    /// </summary>
    public const string JwtOption = "JWT";

    /// <summary>
    /// 签发者
    /// </summary>
    public string Issuer { get; set; } = string.Empty;

    /// <summary>
    /// 收发者
    /// </summary>
    public string Audience { get; set; } = string.Empty;

    /// <summary>
    /// 密钥
    /// </summary>
    public string Secret { get; set; } = string.Empty;

    /// <summary>
    /// Token有效期(单位:分钟)
    /// </summary>
    public int ExpirationTime { get; set; }

    /// <summary>
    /// Token有效刷新时间(单位:分钟)
    /// </summary>
    public int RefreshTime { get; set; }
}

接下来实现具体的业务应用服务实现类
新建名称为UserAppService、RoleAppService、MenuAppService和AuthAppService的用户应用服务类、角色应用服务类、菜单应用服务类和认证授权应用服务类

  • UserAppService
/// <summary>
/// 用户应用服务
/// </summary>
public class UserAppService : ApplicationService, IUserAppService
{
    private readonly ILogger<UserAppService> _logger;
    private readonly ICurrentUser _currentUser;
    private readonly IDistributedCache<UserPermissionCacheItem, Guid> _permissionCache;
    private readonly UserService _userService;
    private readonly IUserRepository _userRepository;
    private readonly IRoleRepository _roleRepository;
    private readonly IRepository<UserRole> _userRoleRepository;

    public UserAppService(ILogger<UserAppService> logger,
        ICurrentUser currentUser,
        IDistributedCache<UserPermissionCacheItem, Guid> permissionCache,
        UserService userService,
        IUserRepository userRepository,
        IRoleRepository roleRepository,
        IRepository<UserRole> userRoleRepository)
    {
        _logger = logger;
        _currentUser = currentUser;
        _permissionCache = permissionCache;
        _userService = userService;
        _userRepository = userRepository;
        _roleRepository = roleRepository;
        _userRoleRepository = userRoleRepository;
    }

    /// <summary>
    /// 获取用户分页列表
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public async Task<PagedResultDto<UserDto>> GetListAsync(UserQueryDto input)
    {
        string fieldToSort = _userRepository.GenerateSortExpression(input.SortField, input.SortType, typeof(User));

        var query = await _userRepository.GetQueryableAsync();
        var list = await AsyncExecuter.ToListAsync(query);
        list = list
            .WhereIf(!string.IsNullOrEmpty(input.Account) && !string.IsNullOrWhiteSpace(input.Account),
                user => user.Account.Contains(input.Account!))
            .WhereIf(!string.IsNullOrEmpty(input.Name) && !string.IsNullOrWhiteSpace(input.Name),
                user => user.Name.Contains(input.Name!))
            .OrderBy(user => user.CreationTime)
            .ToList();
        var totalCount = list.Count;
        list = list.Skip((input.PageIndex - 1) * input.PageSize).Take(input.PageSize).ToList();

        var userIds = list.Select(user => user.Id);
        var userRoles = await _userRoleRepository.GetListAsync(rel => userIds.Contains(rel.UserId));
        var roleIds = userRoles.Select(rel => rel.RoleId).Distinct().ToList();
        var roles = await _roleRepository.GetListAsync(role => roleIds.Contains(role.Id));
        var listDto = list.Select(user =>
        {
            var userDto = ObjectMapper.Map<User, UserDto>(user);
            userDto.RoleIds = userRoles.Where(rel => rel.UserId == user.Id).Select(rel => rel.RoleId).ToList();
            userDto.RoleNames = roles.Where(x => userDto.RoleIds.Contains(x.Id)).Select(y => y.Name).ToList();
            return userDto;
        }).ToList();

        return new PagedResultDto<UserDto>(totalCount, listDto);
    }

    /// <summary>
    /// 新增用户
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public async Task<UserDto> CreateAsync(UserCreateDto input)
    {
        var user = await _userService.AddAsync(
            input.Account,
            input.Password,
            input.Name,
            input.Contact,
            input.Address);
        var roles = await _roleRepository.GetListAsync(role => input.RoleIds.Contains(role.Id));
        var notExistRoles = input.RoleIds.Except(roles.Select(x => x.Id));
        if (notExistRoles.Any())
        {
            throw new UserFriendlyException("角色不存在");
        }
        _userService.BindRoles(user, input.RoleIds);
        user = await _userRepository.InsertAsync(user);
        var userDto = ObjectMapper.Map<User, UserDto>(user);
        return userDto;
    }

    /// <summary>
    /// 更新用户
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public async Task<UserDto> UpdateAsync(UserUpdateDto input)
    {
        var user = await _userService.ModifyAsync(
            input.Id,
            input.Password,
            input.Name,
            input.Contact,
            input.Address);
        var roles = await _roleRepository.GetListAsync(role => input.RoleIds.Contains(role.Id));
        var notExistRoles = input.RoleIds.Except(roles.Select(x => x.Id));
        if (notExistRoles.Any())
        {
            throw new UserFriendlyException("角色不存在");
        }
        _userService.BindRoles(user, input.RoleIds);
        user = await _userRepository.UpdateAsync(user);
        var userDto = ObjectMapper.Map<User, UserDto>(user);
        return userDto;
    }

    /// <summary>
    /// 获取用户
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public async Task<UserDto> GetAsync(Guid id)
    {
        var user = await _userRepository.FindAsync(id);
        if (user == null)
        {
            throw new UserFriendlyException("用户不存在");
        }
        var userDto = ObjectMapper.Map<User, UserDto>(user);
        userDto.RoleIds = user.UserRoles.Select(x => x.RoleId).ToList();
        var roles = await _roleRepository.GetListAsync(role => userDto.RoleIds.Contains(role.Id));
        userDto.RoleNames = roles.Select(role => role.Name).ToList();
        return userDto;
    }

    /// <summary>
    /// 删除用户
    /// </summary>
    /// <param name="ids"></param>
    /// <returns></returns>
    public async Task DeleteAsync([FromBody] List<Guid> ids)
    {
        var users = await _userRepository.GetListAsync(user => ids.Contains(user.Id));
        foreach (var user in users)
        {
            var account = user.Account;
            if (account == UserConsts.AdminAccount)
            {
                throw new UserFriendlyException("不可操作管理员用户");
            }
            if (_currentUser.Id.HasValue)
            {
                if (ids.Contains(_currentUser.Id.Value))
                {
                    throw new UserFriendlyException("不可删除当前用户");
                }
            }
        }
        await _userRepository.DeleteAsync(user => ids.Contains(user.Id));
        await _permissionCache.RemoveManyAsync(ids);
    }

    /// <summary>
    /// 解冻用户
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public async Task<UserDto> UnfreezeAsync(Guid id)
    {
        var user = await _userService.UnfreezeAsync(id);
        user = await _userRepository.UpdateAsync(user);
        var userDto = ObjectMapper.Map<User, UserDto>(user);
        return userDto;
    }
}
  • RoleAppService
    /// <summary>
    /// 角色应用服务
    /// </summary>
    public class RoleAppService : ApplicationService, IRoleAppService
    {
        private readonly RoleService _roleService;
        private readonly IRoleRepository _roleRepository;
        private readonly IRepository<UserRole> _userRoleRepository;

        public RoleAppService(
            RoleService roleService,
            IRoleRepository roleRepository,
            IRepository<UserRole> userRoleRepository
)
        {
            _roleService = roleService;
            _roleRepository = roleRepository;
            _userRoleRepository = userRoleRepository;
        }

        /// <summary>
        /// 获取角色分页列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<PagedResultDto<RoleDto>> GetListAsync(RoleQueryDto input)
        {
            string fieldToSort = _roleRepository.GenerateSortExpression(input.SortField, input.SortType, typeof(Role));

            var query = await _roleRepository.GetQueryableAsync();
            query = query
                .WhereIf(!string.IsNullOrEmpty(input.Name) && !string.IsNullOrWhiteSpace(input.Name),
                role => role.Name.Contains(input.Name!))
                .OrderByIf<Role, IQueryable<Role>>(!string.IsNullOrEmpty(fieldToSort),
                fieldToSort);
            var totalCount = query.Count();
            query = query.Skip((input.PageIndex - 1) * input.PageSize).Take(input.PageSize);

            var list = await AsyncExecuter.ToListAsync(query);
            var listDto = list.Select(role =>
            {
                var roleDto = ObjectMapper.Map<Role, RoleDto>(role);
                return roleDto;
            }).ToList();

            return new PagedResultDto<RoleDto>(totalCount, listDto);
        }

        /// <summary>
        /// 新增角色
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<RoleDto> CreateAsync(RoleCreateDto input)
        {
            var role = await _roleService.AddAsync(
                input.Code,
                input.Name,
                input.Remark);
            role = await _roleRepository.InsertAsync(role);
            var roleDto = ObjectMapper.Map<Role, RoleDto>(role);
            return roleDto;
        }

        /// <summary>
        /// 更新角色
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<RoleDto> UpdateAsync(RoleUpdateDto input)
        {
            var role = await _roleService.ModifyAsync(
                input.Id,
                input.Name,
                input.Remark);
            role = await _roleRepository.UpdateAsync(role);
            var roleDto = ObjectMapper.Map<Role, RoleDto>(role);
            return roleDto;
        }

        /// <summary>
        /// 获取角色
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<RoleDto> GetAsync(Guid id)
        {
            var role = await _roleRepository.FindAsync(id);
            if (role == null)
            {
                throw new UserFriendlyException("角色不存在");
            }
            var roleDto = ObjectMapper.Map<Role, RoleDto>(role);
            return roleDto;
        }

        /// <summary>
        /// 删除角色
        /// </summary>
        /// <param name="ids"></param>
        /// <returns></returns>
        public async Task DeleteAsync([FromBody] List<Guid> ids)
        {
            var userRoles = await _userRoleRepository.GetListAsync(rel => ids.Contains(rel.RoleId));
            if (userRoles.Count > 0)
            {
                throw new UserFriendlyException("角色已被使用,不可删除");
            }
            await _roleRepository.DeleteAsync(role => ids.Contains(role.Id));
        }

        /// <summary>
        /// 获取所有角色
        /// </summary>
        /// <returns></returns>
        public async Task<List<RoleMapDto>> GetAllRoleAsync()
        {
            var roles = await _roleRepository.GetListAsync();
            return ObjectMapper.Map<List<Role>, List<RoleMapDto>>(roles);
        }
    }
  • MenuAppService
/// <summary>
/// 菜单应用服务
/// </summary>
public class MenuAppService : ApplicationService, IMenuAppService
{
    private readonly MenuService _menuService;
    private readonly RoleService _roleService;
    private readonly IRoleRepository _roleRepository;
    private readonly IMenuRepository _menuRepository;

    public MenuAppService(
        MenuService menuService,
        RoleService roleService,
        IRoleRepository roleRepository,
        IMenuRepository menuRepository)
    {
        _menuService = menuService;
        _roleService = roleService;
        _roleRepository = roleRepository;
        _menuRepository = menuRepository;
    }

    /// <summary>
    /// 根据角色编码获取权限
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public async Task<PermissionDto> GetPermissionAsync(Guid id)
    {
        var permissionDto = new PermissionDto();
        var role = await _roleRepository.FindAsync(id);
        if (role == null)
        {
            throw new UserFriendlyException("角色不存在");
        }
        var menuCodes = role.RoleMenus.Select(x => x.MenuCode).ToList();
        var allMenus = await _menuRepository.GetListAsync();
        var menus = allMenus.Where(x => menuCodes.Contains(x.Code));
        permissionDto.CheckedMenu = menus.Where(x => x.Type == "C" || x.Type == "M").Select(y => y.Code).ToList();
        permissionDto.CheckedModule = menus.Where(x => x.Type == "F").Select(y => y.Code).ToList();
        var menuDtos = ObjectMapper.Map<List<Menu>, List<MenuDto>>(allMenus);
        menuDtos = menuDtos.OrderBy(x => x.Sort).ToList();
        permissionDto.MenuModule = [];
        menuDtos.ForEach(item =>
        {
            permissionDto.MenuModule.Add(new MenuModuleDto
            {
                Code = item.Code,
                ParentCode = item.ParentCode,
                Name = item.Name,
                Type = item.Type
            });
        });
        return permissionDto;
    }

    /// <summary>
    /// 更新权限
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public async Task UpdateAsync(PermissionUpdateDto input)
    {
        var role = await _roleRepository.FindAsync(input.RoleId);
        if (role == null)
        {
            throw new UserFriendlyException("角色不存在");
        }
        _roleService.BindMenus(role, input.MenuModules);
        await _roleRepository.UpdateAsync(role);
    }
}
  • AuthAppService
public class AuthAppService : ApplicationService, IAuthAppService
{
    private readonly ILogger<AuthAppService> _logger;
    private readonly JwtOptions _jwtOptions;
    private readonly ICurrentUser _currentUser;
    private readonly IDistributedCache<string> _captchaCache;
    private readonly IDistributedCache<UserPermissionCacheItem, Guid> _permissionCache;
    private readonly UserService _userService;
    private readonly MenuService _menuService;
    private readonly IUserRepository _userRepository;

    public AuthAppService(
        ILogger<AuthAppService> logger,
        IOptions<JwtOptions> jwtOptions,
        ICurrentUser currentUser,
        IDistributedCache<string> captchaCache,
        IDistributedCache<UserPermissionCacheItem, Guid> permissionCache,
        UserService userService,
        MenuService menuService,
        IUserRepository userRepository)
    {
        _logger = logger;
        _jwtOptions = jwtOptions.Value;
        _currentUser = currentUser;
        _captchaCache = captchaCache;
        _permissionCache = permissionCache;
        _userService = userService;
        _menuService = menuService;
        _userRepository = userRepository;
    }

    /// <summary>
    /// 登录
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    [AllowAnonymous]
    [IgnoreAntiforgeryToken]
    [UnitOfWork(IsDisabled = true)]
    public async Task<LoginUserDto> LoginAsync(UserLoginDto input)
    {
        string? captchaCode = await _captchaCache.GetAsync(input.CaptchaTime);
        if (string.IsNullOrEmpty(captchaCode))
        {
            throw new UserFriendlyException("验证码已过期");
        }
        if (!captchaCode.ToUpper().Equals(input.Captcha.ToUpper()))
        {
            throw new UserFriendlyException("验证码错误");
        }

        var user = await _userService.LoginCheckAsync(input.UserAccount, input.Password);

        var userPermissionCacheItem = await _permissionCache.GetOrAddAsync(
            user.Id,
            async () => await GetUserPermissionAsync(user)
        ) ?? throw new UserFriendlyException("获取登录用户权限异常");
        var userToken = new UserTokenDto
        {
            UserId = user.Id,
            UserAccount = user.Account,
            UserName = user.Name,
            RoleCode = userPermissionCacheItem.Role,
            RoleName = userPermissionCacheItem.RoleName
        };
        var accessToken = GetAccessToken(userToken);
        var refreshToken = GetRefreshToken(userToken);

        await _captchaCache.RemoveAsync(input.CaptchaTime);

        return new LoginUserDto
        {
            UserId = user.Id,
            UserAccount = user.Account,
            UserName = user.Name,
            Role = userToken.RoleCode,
            RoleName = userToken.RoleName,
            AccessToken = accessToken.AccessToken,
            ExpiresIn = accessToken.ExpiresIn,
            ExpiresTime = accessToken.ExpiresTime,
            RefreshToken = refreshToken.RefreshToken,
            RefreshExpiresIn = refreshToken.RefreshExpiresIn,
            RefreshExpiresTime = refreshToken.RefreshExpiresTime
        };
    }

    /// <summary>
    /// 获取当前用户
    /// </summary>
    /// <returns></returns>
    public async Task<UserPermissionDto> GetUserAsync()
    {
        if (!_currentUser.IsAuthenticated)
        {
            throw new UserFriendlyException("获取当前用户权限异常");
        }
        var userPermissionCacheItem = await _permissionCache.GetOrAddAsync(
            _currentUser.Id!.Value,
            async () =>
            {
                var user = await _userRepository.GetAsync(user => user.Id == _currentUser.Id!.Value);
                return await GetUserPermissionAsync(user);
            }
        ) ?? throw new UserFriendlyException("获取缓存用户权限异常");
        return ObjectMapper.Map<UserPermissionCacheItem, UserPermissionDto>(userPermissionCacheItem);
    }

    /// <summary>
    /// 获取验证码
    /// </summary>
    /// <returns></returns>
    [AllowAnonymous]
    public async Task<CaptchaDto> GetCaptchaAsync()
    {
        var captchaCode = CaptchaHelper.GenerateRandomCaptcha();
        var dateTimeNow = DateTime.UtcNow;
        var key = dateTimeNow.ToTimestampMillis().ToString();
        await _captchaCache.SetAsync(key, captchaCode, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
        });
        return new CaptchaDto
        {
            Key = key,
            Text = captchaCode,
            Img = Convert.ToBase64String(CaptchaHelper.CreateCaptchaImage(captchaCode))
        };
    }

    /// <summary>
    /// 刷新令牌
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    [AllowAnonymous]
    public async Task<LoginUserDto> RefreshAsync(RefreshTokenCreateDto input)
    {
        var claim = ValidateToken(
            input.RefreshToken,
            _jwtOptions.Secret,
            _jwtOptions.Audience,
            _jwtOptions.Issuer);

        if (claim.Count <= 2 || !Guid.TryParse(claim[2].Value, out Guid userId))
        {
            throw new UserFriendlyException("无效的令牌");
        }

        var userPermissionCacheItem = await _permissionCache.GetOrAddAsync(
            userId,
            async () =>
            {
                var user = await _userRepository.GetAsync(user => user.Id == userId);
                return await GetUserPermissionAsync(user);
            }
        ) ?? throw new UserFriendlyException("获取缓存用户权限异常");

        var userToken = new UserTokenDto
        {
            UserId = userId,
            UserAccount = userPermissionCacheItem.UserAccount,
            UserName = userPermissionCacheItem.UserName,
            RoleCode = userPermissionCacheItem.Role,
            RoleName = userPermissionCacheItem.RoleName
        };

        var accessToken = GetAccessToken(userToken);
        var refreshToken = GetRefreshToken(userToken);

        return new LoginUserDto
        {
            UserId = userId,
            UserAccount = userPermissionCacheItem.UserAccount,
            UserName = userPermissionCacheItem.UserName,
            Role = userToken.RoleCode,
            RoleName = userToken.RoleName,
            AccessToken = accessToken.AccessToken,
            ExpiresIn = accessToken.ExpiresIn,
            ExpiresTime = accessToken.ExpiresTime,
            RefreshToken = refreshToken.RefreshToken,
            RefreshExpiresIn = refreshToken.RefreshExpiresIn,
            RefreshExpiresTime = refreshToken.RefreshExpiresTime
        };
    }

    /// <summary>
    /// 获取用户权限
    /// </summary>
    /// <param name="user"></param>
    /// <returns></returns>
    private async Task<UserPermissionCacheItem> GetUserPermissionAsync(User user)
    {
        var userPermissionCacheItem = await _menuService.GetPermissionAsync(user);
        return userPermissionCacheItem;
    }

    /// <summary>
    /// 获取访问令牌
    /// </summary>
    /// <param name="userToken"></param>
    /// <returns></returns>
    private AccessTokenDto GetAccessToken(UserTokenDto userToken)
    {
        string secret = _jwtOptions.Secret;
        var key = Encoding.UTF8.GetBytes(secret);
        var authTime = DateTime.UtcNow; // 授权时间
        var expiresIn = TimeSpan.FromMinutes(_jwtOptions.ExpirationTime);
        DateTime expires = authTime.Add(expiresIn); // 过期时间

        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenDescripor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity([
                new Claim(AuthConsts.Audience, _jwtOptions.Audience),
                new Claim(AuthConsts.Issuer, _jwtOptions.Issuer),
                new Claim(AbpClaimTypes.UserId, userToken.UserId.ToString()),
                new Claim(AuthConsts.UserAccount, userToken.UserAccount),
                new Claim(AbpClaimTypes.UserName, userToken.UserName),
                new Claim(AbpClaimTypes.Role, userToken.RoleName),
                new Claim(AuthConsts.Subject, AuthConsts.ValidateType)
            ]),
            Expires = expires,
            // 对称秘钥SymmetricSecurityKey
            // 签名证书(秘钥,加密算法)SecurityAlgorithms
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
        };
        var token = tokenHandler.CreateToken(tokenDescripor);
        var tokenString = tokenHandler.WriteToken(token);
        var accessToken = new AccessTokenDto
        {
            AccessToken = tokenString,
            ExpiresIn = (int)expiresIn.TotalMinutes,
            ExpiresTime = expires,
        };
        return accessToken;
    }

    /// <summary>
    /// 获取刷新令牌
    /// </summary>
    /// <param name="userToken"></param>
    /// <returns></returns>
    private RefreshTokenDto GetRefreshToken(UserTokenDto userToken)
    {
        string secret = _jwtOptions.Secret;
        var key = Encoding.UTF8.GetBytes(secret);
        var authTime = DateTime.UtcNow; // 授权时间
        var expiresIn = TimeSpan.FromMinutes(_jwtOptions.RefreshTime);
        DateTime expires = authTime.Add(expiresIn); // 过期时间

        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenDescripor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity([
                new Claim(AuthConsts.Audience, _jwtOptions.Audience),
                new Claim(AuthConsts.Issuer, _jwtOptions.Issuer),
                new Claim(AbpClaimTypes.UserId, userToken.UserId.ToString()),
                new Claim(AuthConsts.UserAccount, userToken.UserAccount),
                new Claim(AbpClaimTypes.UserName, userToken.UserName),
                new Claim(AbpClaimTypes.Role, userToken.RoleName),
                new Claim(AuthConsts.Subject, AuthConsts.ValidateType)
            ]),
            Expires = expires,
            // 对称秘钥SymmetricSecurityKey
            // 签名证书(秘钥,加密算法)SecurityAlgorithms
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
        };
        var token = tokenHandler.CreateToken(tokenDescripor);
        var tokenString = tokenHandler.WriteToken(token);
        var refreshToken = new RefreshTokenDto
        {
            RefreshToken = tokenString,
            RefreshExpiresIn = (int)expiresIn.TotalMinutes,
            RefreshExpiresTime = expires,
        };
        return refreshToken;
    }

    /// <summary>
    /// 校验令牌
    /// </summary>
    /// <param name="token"></param>
    /// <param name="secret"></param>
    /// <param name="audience"></param>
    /// <param name="issuer"></param>
    /// <returns></returns>
    private List<Claim> ValidateToken(string token, string secret, string audience, string issuer)
    {
        var claims = new List<Claim>();
        if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(secret))
        {
            throw new UserFriendlyException("无效的令牌");
        }
        try
        {
            JwtSecurityToken jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(token);
            if (jwtToken == null)
            {
                throw new UserFriendlyException("无效的令牌");
            }
            //string audience = jwtToken.Claims.ToList()[0].Value;
            //string issuer = jwtToken.Claims.ToList()[1].Value;
            var keyByteArray = Encoding.UTF8.GetBytes(secret);
            new JwtSecurityTokenHandler().ValidateToken(token, new TokenValidationParameters()
            {
                RequireExpirationTime = true,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(keyByteArray),
                ValidateAudience = true,
                ValidAudience = audience,
                ValidateIssuer = true,
                ValidIssuer = issuer,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);
            if (jwtToken.Payload.Claims is List<Claim> claimlist)
            {
                claims = claimlist;
            }
        }
        catch (SecurityTokenExpiredException ex)
        {
            _logger.LogError(ex, "令牌超时,登录失效");
            throw new UserFriendlyException("令牌超时,登录失效");
        }
        catch (SecurityTokenInvalidLifetimeException ex)
        {
            _logger.LogError(ex, "令牌超时,登录失效");
            throw new UserFriendlyException("令牌超时,登录失效");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "无效的令牌");
            throw new UserFriendlyException("无效的令牌");
        }
        return claims;
    }
}

在程序包管理控制台选中General.Backend.Domain.Shared,执行以下命令安装图形相关的Nuget包,用于生成验证码图形

Install-Package SkiaSharp.NativeAssets.Linux.NoDependencies -v 2.88.8
Install-Package System.Drawing.Common -v 8.0.0

在General.Backend.Domain.Shared标准类库中,新建名称为Extensions和Helper的文件夹,用户存放扩展类和帮助类
在Extensions文件夹下,新建名称为DataExtension的数据扩展类

/// <summary>
/// 数据扩展
/// </summary>
public static class DataExtension
{
    /// <summary>
    /// 时间戳转换为时间
    /// </summary>
    /// <param name="timestampMillis"></param>
    /// <param name="dateTimeKind"></param>
    /// <returns></returns>
    public static DateTime ToDateTime(this long timestampMillis, DateTimeKind dateTimeKind = DateTimeKind.Utc)
    {
        var dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, dateTimeKind).AddMilliseconds(timestampMillis);
        return dateTime;
    }

    /// <summary>
    /// 时间转换为时间戳
    /// </summary>
    /// <param name="dateTime"></param>
    /// <param name="dateTimeKind"></param>
    /// <returns></returns>
    public static long ToTimestampMillis(this DateTime dateTime, DateTimeKind dateTimeKind = DateTimeKind.Utc)
    {
        var utcDateTime = new DateTime(1970, 1, 1, 0, 0, 0, dateTimeKind);
        TimeSpan timeSpan = dateTime - utcDateTime;
        long timestampMillis = Convert.ToInt64(timeSpan.TotalMilliseconds);
        return timestampMillis;
    }
}

在Helper文件夹下,新建名称为CaptchaHelper的验证码生成类

public partial class CaptchaHelper
{
    private const string Letters = "1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,J,K,L,M,N,P,Q,R,S,T,U,V,W,X,Y,Z";

    /// <summary>
    /// 生成随机验证码字符
    /// </summary>
    /// <param name="codeLength">验证码位数</param>
    /// <returns></returns>
    public static string GenerateRandomCaptcha(int codeLength = 4)
    {
        var array = Letters.Split(new[] { ',' });
        var random = new Random();
        var temp = -1;
        var captcheCode = string.Empty;
        for (int i = 0; i < codeLength; i++)
        {
            if (temp != -1)
            {
                random = new Random(i * temp * unchecked((int)DateTime.UtcNow.Ticks));
            }
            var index = random.Next(array.Length);
            if (temp != -1 && temp == index)
            {
                return GenerateRandomCaptcha(codeLength);
            }
            temp = index;
            captcheCode += array[index];
        }
        return captcheCode;
    }

    /// <summary>
    /// 生成验证码图片
    /// </summary>
    /// <param name="randomCode"></param>
    /// <returns></returns>
    public static byte[] CreateCaptchaImage(string randomCode = "")
    {
        if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
            System.Runtime.InteropServices.OSPlatform.Windows))
        {
            return CreateCaptchaImageForWindows(randomCode);
        }
        else
        {
            try
            {
                var verifyCode = new Captcha
                {
                    SetIsBackgroundLine = true,
                    SetVerifyCodeText = randomCode,

                    SetIsRandomColor = true,
                    SetRandomAngle = 30
                };
                verifyCode.SetWith = verifyCode.SetVerifyCodeText.Length * 16;
                verifyCode.SetHeight = 28;
                verifyCode.SetFontSize = 14;
                verifyCode.SetForeNoisePointCount = 0;
                verifyCode.SetDisturbStyleLine = false;

                var bytes = verifyCode.GetVerifyCodeImage();
                return bytes;
            }
            catch (Exception)
            {
                throw;
            }
        }
    }

    /// <summary>
    /// 生成验证码图片
    /// </summary>
    /// <param name="randomCode"></param>
    /// <returns></returns>
    [System.Runtime.Versioning.SupportedOSPlatform("windows")]
    public static byte[] CreateCaptchaImageForWindows(string randomCode)
    {
        // 随机转动角度
        const int randAngle = 45;
        int mapwidth = randomCode.Length * 16;
        // 创建图片背景
        var map = new Bitmap(mapwidth, 28);
        Graphics graph = Graphics.FromImage(map);
        // 清除画面,填充背景
        graph.Clear(Color.AliceBlue);
        var random = new Random();
        // 绘制干扰曲线
        for (int i = 0; i < 2; i++)
        {
            var p1 = new Point(0, random.Next(map.Height));
            var p2 = new Point(random.Next(map.Width), random.Next(map.Height));
            var p3 = new Point(random.Next(map.Width), random.Next(map.Height));
            var p4 = new Point(map.Width, random.Next(map.Height));
            Point[] p = { p1, p2, p3, p4 };
            using (var pen = new Pen(Color.Gray, 1))
            {
                graph.DrawBeziers(pen, p);
            }
        }
        // 文字距中
        using (StringFormat format = new StringFormat(StringFormatFlags.NoClip))
        {
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;
            // 定义颜色
            Color[] c = { Color.Black, Color.Red, Color.DarkBlue, 
                Color.Green, Color.Orange, Color.Brown, Color.DarkCyan, Color.Purple };
            // 定义字体
            string[] fonts = { "Verdana", "Microsoft Sans Serif", "Comic Sans MS", "Arial", "宋体" };
            int cindex = random.Next(7);
            // 验证码旋转,防止机器识别
            // 拆散字符串成单字符数组
            char[] chars = randomCode.ToCharArray();
            foreach (char t in chars)
            {
                int findex = random.Next(5);
                // 字体样式(参数2为字体大小)
                using (Font font = new Font(fonts[findex], 14, FontStyle.Bold))
                {
                    using (Brush brush = new SolidBrush(c[cindex]))
                    {
                        Point dot = new Point(14, 14);
                        // 转动的度数
                        float angle = random.Next(-randAngle, randAngle);
                        if (t == '+' || t == '-' || t == '*')
                        {
                            // 加减乘运算符不进行旋转
                            // 移动光标到指定位置
                            graph.TranslateTransform(dot.X, dot.Y);
                            graph.DrawString(t.ToString(), font, brush, 1, 1, format);
                            // 移动光标到指定位置,每个字符紧凑显示,避免被软件识别
                            graph.TranslateTransform(-2, -dot.Y);
                        }
                        else
                        {
                            // 移动光标到指定位置
                            graph.TranslateTransform(dot.X, dot.Y);
                            graph.RotateTransform(angle);
                            graph.DrawString(t.ToString(), font, brush, 1, 1, format);
                            // 转回去
                            graph.RotateTransform(-angle);
                            // 移动光标到指定位置,每个字符紧凑显示,避免被软件识别
                            graph.TranslateTransform(-2, -dot.Y);
                        }
                    }
                }
            }
        }
        // 生成图片
        using (var ms = new MemoryStream())
        {
            map.Save(ms, ImageFormat.Gif);
            graph.Dispose();
            map.Dispose();
            return ms.GetBuffer();
        }
    }

    public class Captcha
    {
        private readonly Random objRandom = new();

        /// <summary>
        /// //验证码长度
        /// </summary>
        public int SetLength { get; set; } = 4;

        /// <summary>
        /// 验证码字符串
        /// </summary>
        public string SetVerifyCodeText { get; set; } = string.Empty;

        /// <summary>
        /// 是否加入小写字母
        /// </summary>
        public bool SetAddLowerLetter { get; set; } = true;

        /// <summary>
        /// 是否加入大写字母
        /// </summary>
        public bool SetAddUpperLetter { get; set; } = true;

        /// <summary>
        /// 字体大小
        /// </summary>
        public int SetFontSize { get; set; } = 36;

        /// <summary>
        ///  //字体颜色
        /// </summary>
        public SKColor SetFontColor { get; set; } = SKColors.Blue;

        /// <summary>
        /// 字体类型
        /// </summary>
        public string SetFontFamily = "Verdana";

        /// <summary>
        /// 背景色
        /// </summary>
        public SKColor SetBackgroundColor { get; set; } = SKColors.AliceBlue;

        /// <summary>
        /// 是否加入背景线
        /// </summary>
        public bool SetIsBackgroundLine { get; set; }

        /// <summary>
        /// 设置干扰线
        /// </summary>
        public bool SetDisturbStyleLine { get; set; }

        /// <summary>
        /// 前景噪点数量
        /// </summary>
        public int SetForeNoisePointCount { get; set; } = 2;

        /// <summary>
        /// 随机码的旋转角度
        /// </summary>
        public int SetRandomAngle { get; set; } = 40;

        /// <summary>
        /// 是否随机字体颜色
        /// </summary>
        public bool SetIsRandomColor { get; set; } = true;

        /// <summary>
        /// 图片宽度
        /// </summary>
        public int SetWith { get; set; } = 200;

        /// <summary>
        /// 图片高度
        /// </summary>
        public int SetHeight { get; set; } = 40;

        /// <summary>
        /// 问题验证码答案,适用于运算符
        /// </summary>
        public string VerifyCodeResult { get; private set; } = string.Empty;

        public Captcha(int length = 4, bool isOperation = false)
        {
            if (isOperation)
            {
                var dic = GetQuestion();
                SetVerifyCodeText = dic.Key;
                VerifyCodeResult = dic.Value;
                SetRandomAngle = 0;
            }
            else
            {
                SetLength = length;
                GetVerifyCodeText();
            }
            SetWith = SetVerifyCodeText.Length * SetFontSize;
            SetHeight = Convert.ToInt32(60.0 / 100 * SetFontSize + SetFontSize);
            InitColors();
        }

        /// <summary>
        /// 得到验证码字符串
        /// </summary>
        private void GetVerifyCodeText()
        {
            // 没有外部输入验证码时随机生成
            if (string.IsNullOrEmpty(SetVerifyCodeText))
            {
                StringBuilder objStringBuilder = new StringBuilder();
                // 加入数字1-9
                for (int i = 1; i <= 9; i++)
                {
                    objStringBuilder.Append(i.ToString());
                }
                // 加入大写字母A-Z,不包括O
                if (SetAddUpperLetter)
                {
                    char temp = ' ';
                    for (int i = 0; i < 26; i++)
                    {
                        temp = Convert.ToChar(i + 65);
                        // 如果生成的字母不是'O'
                        if (!temp.Equals('O'))
                        {
                            objStringBuilder.Append(temp);
                        }
                    }
                }
                // 加入小写字母a-z,不包括o
                if (SetAddLowerLetter)
                {
                    char temp = ' ';
                    for (int i = 0; i < 26; i++)
                    {
                        temp = Convert.ToChar(i + 97);
                        //如果生成的字母不是'o'
                        if (!temp.Equals('o'))
                        {
                            objStringBuilder.Append(temp);
                        }
                    }
                }
                // 生成验证码字符串
                {
                    int index = 0;
                    for (int i = 0; i < SetLength; i++)
                    {
                        index = objRandom.Next(0, objStringBuilder.Length);
                        SetVerifyCodeText += objStringBuilder[index];
                        objStringBuilder.Remove(index, 1);
                    }
                }
            }
        }

        /// <summary>
        /// 获取随机颜色
        /// </summary>
        /// <returns></returns>
        private SKColor GetRandomColor()
        {
            Random random = new Random();
            return Colors2[random.Next(Colors2.Count)];
        }

        /// <summary>
        /// 获取问题
        /// </summary>
        /// <param name="questionList">默认数字加减验证</param>
        /// <returns></returns>
        public KeyValuePair<string, string> GetQuestion(Dictionary<string, string> questionList = null)
        {
            if (questionList == null)
            {
                questionList = new Dictionary<string, string>();
                var operArray = new string[] { "+", "*", "-", "/" };
                var left = objRandom.Next(0, 10);
                var right = objRandom.Next(0, 10);
                var oper = operArray[objRandom.Next(0, operArray.Length)];
                string key = string.Empty, val = string.Empty;
                switch (oper)
                {
                    case "+":
                        key = string.Format("{0}+{1}=?", left, right);
                        val = (left + right).ToString();
                        questionList.Add(key, val);
                        break;
                    case "*":
                        key = string.Format("{0}×{1}=?", left, right);
                        val = (left * right).ToString();
                        questionList.Add(key, val);
                        break;
                    case "-":
                        if (left < right)
                        {
                            var intTemp = left;
                            left = right;
                            right = intTemp;
                        }
                        questionList.Add(left + "-" + right + "= ?", (left - right).ToString());
                        break;
                    case "/":
                        right = objRandom.Next(1, 10);
                        left = right * objRandom.Next(1, 10);
                        questionList.Add(left + "÷" + right + "= ?", (left / right).ToString());
                        break;
                }
            }
            return questionList.ToList()[objRandom.Next(0, questionList.Count)];
        }

        /// <summary>
        /// 干扰线的颜色集合
        /// </summary>
        private List<SKColor> Colors { get; set; } = new List<SKColor>();

        private List<SKColor> Colors2 { get; set; } = new List<SKColor>();

        public void InitColors()
        {
            Colors2 = new List<SKColor>
            {
                SKColors.Orange,
                SKColors.Purple,
                SKColors.DarkBlue,
                SKColors.Green,
                SKColors.RoyalBlue,
                SKColors.Black,
                SKColors.Brown
            };

            Colors = new List<SKColor>
            {
                SKColors.AliceBlue,
                SKColors.PaleGreen,
                SKColors.PaleGoldenrod,
                SKColors.Orchid,
                SKColors.OrangeRed,
                SKColors.Orange,
                SKColors.OliveDrab,
                SKColors.Olive,
                SKColors.OldLace,
                SKColors.Navy,
                SKColors.NavajoWhite,
                SKColors.Moccasin,
                SKColors.MistyRose,
                SKColors.MintCream,
                SKColors.MidnightBlue,
                SKColors.MediumVioletRed,
                SKColors.MediumTurquoise,
                SKColors.MediumSpringGreen,
                SKColors.LightSlateGray,
                SKColors.LightSteelBlue,
                SKColors.LightYellow,
                SKColors.Lime,
                SKColors.LimeGreen,
                SKColors.Linen,
                SKColors.PaleTurquoise,
                SKColors.Magenta,
                SKColors.MediumAquamarine,
                SKColors.MediumBlue,
                SKColors.MediumOrchid,
                SKColors.MediumPurple,
                SKColors.MediumSeaGreen,
                SKColors.MediumSlateBlue,
                SKColors.Maroon,
                SKColors.PaleVioletRed,
                SKColors.PapayaWhip,
                SKColors.PeachPuff,
                SKColors.Snow,
                SKColors.SpringGreen,
                SKColors.SteelBlue,
                SKColors.Tan,
                SKColors.Teal,
                SKColors.Thistle,
                SKColors.SlateGray,
                SKColors.Tomato,
                SKColors.Violet,
                SKColors.Wheat,
                SKColors.White,
                SKColors.WhiteSmoke,
                SKColors.Yellow,
                SKColors.YellowGreen,
                SKColors.Turquoise,
                SKColors.LightSkyBlue,
                SKColors.SlateBlue,
                SKColors.Silver,
                SKColors.Peru,
                SKColors.Pink,
                SKColors.Plum,
                SKColors.PowderBlue,
                SKColors.Purple,
                SKColors.Red,
                SKColors.SkyBlue,
                SKColors.RosyBrown,
                SKColors.SaddleBrown,
                SKColors.Salmon,
                SKColors.SandyBrown,
                SKColors.SeaGreen,
                SKColors.SeaShell,
                SKColors.Sienna,
                SKColors.RoyalBlue,
                SKColors.LightSeaGreen,
                SKColors.LightSalmon,
                SKColors.LightPink,
                SKColors.Crimson,
                SKColors.Cyan,
                SKColors.DarkBlue,
                SKColors.Green,
                SKColors.RoyalBlue,
                SKColors.DarkGoldenrod,
                SKColors.DarkGray,
                SKColors.Cornsilk,
                SKColors.DarkGreen,
                SKColors.DarkMagenta,
                SKColors.DarkOliveGreen,
                SKColors.DarkOrange,
                SKColors.DarkOrchid,
                SKColors.DarkRed,
                SKColors.DarkSalmon,
                SKColors.DarkKhaki,
                SKColors.DarkSeaGreen,
                SKColors.CornflowerBlue,
                SKColors.Chocolate,
                SKColors.AntiqueWhite,
                SKColors.Aqua,
                SKColors.Aquamarine,
                SKColors.Azure,
                SKColors.Beige,
                SKColors.Bisque,
                SKColors.Coral,
                SKColors.Black,
                SKColors.Blue,
                SKColors.BlueViolet,
                SKColors.Brown,
                SKColors.BurlyWood,
                SKColors.CadetBlue,
                SKColors.Chartreuse,
                SKColors.BlanchedAlmond,
                SKColors.Transparent,
                SKColors.DarkSlateBlue,
                SKColors.DarkTurquoise,
                SKColors.IndianRed,
                SKColors.Indigo,
                SKColors.Ivory,
                SKColors.Khaki,
                SKColors.Lavender,
                SKColors.LavenderBlush,
                SKColors.HotPink,
                SKColors.LawnGreen,
                SKColors.LightBlue,
                SKColors.LightCoral,
                SKColors.LightCyan,
                SKColors.LightGoldenrodYellow,
                SKColors.LightGray,
                SKColors.LightGreen,
                SKColors.LemonChiffon,
                SKColors.DarkSlateGray,
                SKColors.Honeydew,
                SKColors.Green,
                SKColors.DarkViolet,
                SKColors.DeepPink,
                SKColors.DeepSkyBlue,
                SKColors.DimGray,
                SKColors.DodgerBlue,
                SKColors.Firebrick,
                SKColors.GreenYellow,
                SKColors.FloralWhite,
                SKColors.Fuchsia,
                SKColors.Gainsboro,
                SKColors.GhostWhite,
                SKColors.Gold,
                SKColors.Goldenrod,
                SKColors.Gray,
                SKColors.ForestGreen
            };
        }

        /// <summary>
        /// 创建画笔
        /// </summary>
        /// <param name="color"></param>
        /// <param name="fontSize"></param>
        /// <returns></returns>
        private static SKPaint CreatePaint(SKColor color, float fontSize)
        {
            SKTypeface font = SKTypeface.FromFamilyName(
                null, 
                SKFontStyleWeight.SemiBold, 
                SKFontStyleWidth.ExtraCondensed, 
                SKFontStyleSlant.Upright);
            var paint = new SKPaint
            {
                IsAntialias = true,
                Color = color,
                Typeface = font,
                TextSize = fontSize
            };
            return paint;
        }

        /// <summary>
        /// 获取验证码
        /// </summary>
        /// <param name="captchaText">验证码文字</param>
        /// <param name="width">图片宽度</param>
        /// <param name="height">图片高度</param>
        /// <param name="lineNum">干扰线数量</param>
        /// <param name="lineStrookeWidth">干扰线宽度</param>
        /// <returns></returns>
        public byte[] GetVerifyCodeImage(int lineNum = 1, int lineStrookeWidth = 1)
        {
            // 创建bitmap位图
            using (var image2d = new SKBitmap(
                SetWith, 
                SetHeight, 
                SKColorType.Bgra8888, 
                SKAlphaType.Premul))
            {
                // 创建画笔
                using (SKCanvas canvas = new SKCanvas(image2d))
                {
                    // 填充背景颜色为白色
                    if (SetIsRandomColor)
                    {
                        SetFontColor = GetRandomColor();
                    }
                    // 填充白色背景
                    canvas.Clear(SetBackgroundColor);
                    AddForeNoisePoint(image2d);
                    AddBackgroundNoisePoint(image2d, canvas);
                    var drawStyle = new SKPaint();
                    drawStyle.IsAntialias = true;
                    drawStyle.TextSize = SetFontSize;
                    char[] chars = SetVerifyCodeText.ToCharArray();
                    for (int i = 0; i < chars.Length; i++)
                    {
                        var font = SKTypeface.FromFamilyName(
                            SetFontFamily, 
                            SKFontStyleWeight.SemiBold, 
                            SKFontStyleWidth.ExtraCondensed, 
                            SKFontStyleSlant.Upright);
                        // 转动的度数
                        float angle = objRandom.Next(-30, 30);
                        canvas.Translate(12, 12);
                        float px = i * SetFontSize;
                        float py = SetHeight / 3;
                        canvas.RotateDegrees(angle, px, py);
                        drawStyle.Typeface = font;
                        drawStyle.Color = SetFontColor;
                        canvas.DrawText(chars[i].ToString(), px, py, drawStyle);
                        canvas.RotateDegrees(-angle, px, py);
                        canvas.Translate(-12, -12);
                    }
                    if (SetDisturbStyleLine)
                    {
                        // 画随机干扰线
                        using (SKPaint disturbStyle = new SKPaint())
                        {
                            Random random = new Random();
                            for (int i = 0; i < lineNum; i++)
                            {
                                disturbStyle.Color = Colors[random.Next(Colors.Count)];
                                disturbStyle.StrokeWidth = lineStrookeWidth;
                                canvas.DrawLine(
                                    random.Next(0, SetWith), 
                                    random.Next(0, SetHeight), 
                                    random.Next(0, SetWith), 
                                    random.Next(0, SetHeight), 
                                    disturbStyle);
                            }
                        }
                    }
                    // 返回图片byte
                    using (SKImage img = SKImage.FromBitmap(image2d))
                    {
                        using (SKData p = img.Encode(SKEncodedImageFormat.Png, 100))
                        {
                            return p.ToArray();
                        }
                    }
                }
            }
        }

        private void AddForeNoisePoint(SKBitmap objBitmap)
        {
            for (int i = 0; i < objBitmap.Width * SetForeNoisePointCount; i++)
            {
                objBitmap.SetPixel(objRandom.Next(
                    objBitmap.Width), 
                    objRandom.Next(objBitmap.Height), 
                    SetFontColor);
            }
        }

        private void AddBackgroundNoisePoint(SKBitmap objBitmap, SKCanvas objGraphics)
        {
            using (SKPaint objPen = CreatePaint(SKColors.Azure, 0))
            {
                for (int i = 0; i < objBitmap.Width * 2; i++)
                {
                    objGraphics.DrawRect(
                        objRandom.Next(objBitmap.Width), 
                        objRandom.Next(objBitmap.Height), 
                        1, 
                        1, 
                        objPen);
                }
            }
            if (SetIsBackgroundLine)
            {
                // 画图片的背景噪音线
                for (var i = 0; i < 12; i++)
                {
                    var x1 = objRandom.Next(objBitmap.Width);
                    var x2 = objRandom.Next(objBitmap.Width);
                    var y1 = objRandom.Next(objBitmap.Height);
                    var y2 = objRandom.Next(objBitmap.Height);

                    objGraphics.DrawLine(x1, y1, x2, y2, CreatePaint(SKColors.Silver, 0));
                }
            }
        }
    }
}

在General.Backend.Domain.Shared中的Consts文件夹下,新建名称为AuthConsts的认证授权常量类

public class AuthConsts
{
    public const string Issuer = "iss";

    public const string Audience = "aud";

    public const string ValidateType = "password";

    public const string Subject = "subject";

    public const string UserAccount = "account";
}

在General.Backend.Domain中的CacheItems文件夹下,新建名称为UserPermissionCacheItem的用户权限缓存类

/// <summary>
/// 用户权限
/// </summary>
public class UserPermissionCacheItem
{
    /// <summary>
    /// 用户Id
    /// </summary>
    public Guid UserId { get; set; }

    /// <summary>
    /// 用户账号
    /// </summary>
    public string UserAccount { get; set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public string UserName { get; set; } = string.Empty;

    /// <summary>
    /// 角色
    /// </summary>
    public string Role { get; set; } = string.Empty;

    /// <summary>
    /// 角色名称
    /// </summary>
    public string RoleName { get; set; } = string.Empty;

    /// <summary>
    /// 授权访问菜单
    /// </summary>
    public List<MenuRouterCacheItem> MenuRouters { get; set; } = new List<MenuRouterCacheItem>();

    /// <summary>
    /// 授权使用功能
    /// </summary>
    public List<string> Modules { get; set; } = [];
}

完善基础仓储的功能,实现一个生成排序方式的方法

  • IBaseRepository
/// <summary>
/// 基础仓储
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TKey"></typeparam>
public interface IBaseRepository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
    public string GenerateSortExpression(string sortField, string sortType, Type type);
}
  • BaseRepository
/// <summary>
/// 基础仓储
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TKey"></typeparam>
public class BaseRepository<TEntity, TKey> : EfCoreRepository<GeneralDbContext, TEntity, TKey> where TEntity : class, IEntity<TKey>
{
    public BaseRepository(IDbContextProvider<GeneralDbContext> dbContextProvider) : base(dbContextProvider)
    {

    }

    public string GenerateSortExpression(string sortField, string sortType, Type type)
    {
        string sortExpression = string.Empty;
        if (IsValidField(sortField, type))
        {
            sortExpression = sortField;
            if (sortType != "asc")
            {
                sortExpression += " desc";
            }
            else
            {
                sortExpression += " asc";
            }
        }
        return sortExpression;
    }

    public bool IsValidField(string field, Type type)
    {
        PropertyInfo[] properties = type.GetProperties();
        foreach (PropertyInfo property in properties)
        {
            if (property.Name.Equals(field))
            {
                return true;
            }
        }
        return false;
    }
}

解决方案的目录结构现如下

在下一章节中,针对仓储层和应用层进行单元测试

posted @ 2025-03-07 17:33  loveraindeme  阅读(70)  评论(0)    收藏  举报