CCITU.Middleware
                             
                            
                                2.2.12
                            
                        
                    dotnet add package CCITU.Middleware --version 2.2.12
NuGet\Install-Package CCITU.Middleware -Version 2.2.12
<PackageReference Include="CCITU.Middleware" Version="2.2.12" />
<PackageVersion Include="CCITU.Middleware" Version="2.2.12" />
<PackageReference Include="CCITU.Middleware" />
paket add CCITU.Middleware --version 2.2.12
#r "nuget: CCITU.Middleware, 2.2.12"
#:package CCITU.Middleware@2.2.12
#addin nuget:?package=CCITU.Middleware&version=2.2.12
#tool nuget:?package=CCITU.Middleware&version=2.2.12
一、ABP.Authentication身份认证
使用场景:
用于登录授权,接口鉴权。
依赖包:已安装
Install-Package JWT -Version 10.0.2
配置:放入appsettings.json文件里
{
    "Authentication": {
        "SecretKey": "用户授权、鉴权的密钥",
        "TokenEffectiveMinutes": Token有效分钟数,
        "RefreshTokenMinutes": Token在过期多少分钟前刷新Token
    }
}
初始化:
1. 注册ABP依赖模块:AbpAuthenticationModule
[DependsOn(typeof(AbpAuthenticationModule))]
public class AbpTestModule:AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var services = context.Services;
        //如果已经在appsettings.json文件里填了配置信息,这里可以不用调用,两种初始化方式用一种就好
        services.AddAbpAuthentication(options =>
        {
            options.SecretKey = "";//用户授权、鉴权的密钥
            options.TokenEffectiveMinutes = 30;//Token有效分钟数
            options.RefreshTokenMinutes = 5;//Token在过期多少分钟前刷新Token
        });
    }
}
过滤器:
AuthenticationAttribute,用于对请求头部的Token进行校验
1. 鉴权
2. 有效期验证
3. 当Token即将过期刷新Token,刷新的Token会放到响应头部中,Key=NewToken
4. 将授权数据写入HttpContext.Items.Add("Auth_Data", "授权数据");
扩展方法:
1. 获取授权数据:string GetAuthenticationData(this HttpContext context)
使用说明:
1. 引入依赖注入实例IAuthenticationService
public interface IAuthenticationService
{
    /// <summary>
    /// 授权
    /// </summary>
    /// <param name="data">授权数据:用于存储跟授权用户相关的数据</param>
    /// <returns>返回Token</returns>
    string Authorization(Dictionary<string, object> data);
    /// <summary>
    /// 鉴权
    /// </summary>
    /// <param name="token">访问令牌</param>
    /// <returns>授权用户相关的数据</returns>
    string Authentication(string token);
    /// <summary>
    /// 是否刷新Token
    /// </summary>
    /// <param name="expTime">token过期时间</param>
    /// <returns></returns>
    bool IsRefreshToke(DateTime expTime);
}
二、ABP.DataEncryption数据加密
使用场景:
用于调用接口时,对请求参数加密处理
依赖包:已安装
Install-Package Portable.BouncyCastle -Version 1.9.0
初始化:
1. 注册ABP依赖模块:AbpDataEncryptionModule
2. 在配置服务容器中添加以下代码
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    services.AddDataEncryption(options =>
    {
        options.RSAPublic = "RSA加密公钥";
        options.RSAPrivate = "RSA加密私钥";
    });
}
前端:
1. RsaPublicKey=获取公钥
2. AesKey=随机生成一个字符串作为对称加密的密钥,16个字符
3. 用AesKey对请求数据进行对称加密,加密后的密文放到请求body里
4. 用公钥RsaPublicKey对密钥AesKey进行非对称加密,加密后的密文放到请求头部里,key=AesKey
5. 请求头部的数据类型=Aes,Content-Type=Aes
后端:
1. 在需要加密的实体类上面加上注解[ModelBinder(BinderType = typeof(DataEncryption.DataEncryptionModelBinder))]
2. 在接口参数左边加上参数注解[FromBody]
三、ABP.DataValidation数据验证
初始化:
1. 注册ABP依赖模块:AbpModelStateValidatorModule
基于数据注解验证:
Required:指定属性不能为空
Range:指定数值属性的范围
RegularExpression:指定属性的正则表达式
MaxLength:指定字符串属性的最大长度
MinLength:指定字符串属性的最小长度
DataType:指定属性的数据类型,例如日期、电子邮件或 URL 等
Compare:指定要比较的属性名称,例如两次输入密码是否一致等
自定义数据注解验证:
NotContainChinese:不包含中文
NotContainSpecialCharacter:不包含特殊字符
NotContainNumber:不包含数字
四、ABP.Hangfire任务调度
使用场景:
用于定时任务,在指定的周期时间内,调用指定的接口
依赖包:已安装
Install-Package Hangfire -Version 1.8.1
Install-Package Hangfire.Dashboard.BasicAuthorization -Version 1.0.2
Install-Package Hangfire.HttpJob -Version 3.7.6
Install-Package Hangfire.HttpJob.Client -Version 1.2.9
Install-Package Hangfire.Redis.StackExchange -Version 1.8.7
配置:放入appsettings.json文件里
{
    "Hangfire": {
        "ConnectionString": "数据库连接字符串",
        "DbType": 2, //数据库类型,1=SqlServer|2=Redis,默认为SqlServer
        "DefaultDatabase": 10, //如果使用的是Redis,可以指定默认使用的数据库,不指定为0
        "UserName": "后台管理授权账号",
        "Password": "后台管理授权密码"
    }
}
初始化:
1. 注册ABP依赖模块:AbpHangfireModule
2. 在配置服务容器中添加以下代码
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    //如果已经在appsettings.json文件里填了配置信息,这里可以不用调用,两种初始化方式用一种就好
    services.AddAbpHangfire(options =>
    {
        options.ConnectionString = "数据库连接字符串";
        options.DbType = "数据库类型,默认为SqlServer";
        options.DefaultDatabase = "如果使用的是Redis,可以指定使用的数据库,默认为0";
        options.UserName = "登录账号";
        options.Password = "登录密码";
    });
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    var app = context.GetApplicationBuilder();
    //如果已经在appsettings.json文件里填了配置信息,这里可以不用调用,两种初始化方式用一种就好
    app.UseAbpHangfireDashboard();
}
后台管理:路由地址/hangfire
1. 添加定时任务
2. 查看定时任务执行结果
五、ABP.LogDashboard日志面板
使用场景:
用于记录日志、查看日志
依赖包:已安装
Install-Package LogDashboard -Version 1.4.8
Install-Package Serilog.AspNetCore -Version 6.1.0
配置:放入appsettings.json文件里
{
    "LogDashboard": {
        "UserName": "后台管理授权账号",
        "Password": "后台管理授权密码"
    }
}
初始化:
1. 注册ABP依赖模块:AbpLogDashboardModule
过滤器:
ApiLogFilterAttribute 记录接口请求响应日志
ErrorLogFilterAttribute 记录系统异常日志
自定义属性:
NotRecordApiLog 不记录日志
NotRecordApiRequestLog 不记录接口请求日志
NotRecordApiResponseLog 不记录接口响应日志
后台管理:路由地址/logdashboard
1. 查看日志
使用说明:
1. 引入依赖注入实例ILogger
记录日志使用方式跟netcore框架自带的ILogger一样
六、ABP.RabbitMQ消息队列
依赖包:已安装
Install-Package RabbitMQ.Client -Version 6.5.0
配置:放入appsettings.json文件里
{
    "RabbitMQ": {
        "Host": "主机地址",
        "Port": 端口号,
        "UserName": "账号",
        "PassWord": "密码",
        "VirtualHost": "/",
        "ChannelOptions":{
            "PrefetchCount":每个消费者一次能预取的消息数量,数值越大内存占用越高
        }
    }
}
初始化:
1. 注册ABP依赖模块:AbpRabbitMQModule
使用说明:
1. 引入依赖注入实例IRabbitMQService
/// <summary>
/// MQ消息队列服务
/// </summary>
public interface IRabbitMQService
{
    /// <summary>
    /// 发送消息
    /// </summary>
    /// <param name="queueName">队列名称</param>
    /// <param name="msg">消息</param>
    void Publish(string queueName, string msg);
    /// <summary>
    /// 发送消息
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="queueName"></param>
    /// <param name="msg"></param>
    void Publish<T>(string queueName, T msg);
    /// <summary>
    /// 消费消息
    /// </summary>
    /// <param name="queueName">队列名称</param>
    /// <param name="consumeFunc">消息回调函数:返回true消费成功,返回false消费失败</param>
    /// <param name="maxRetryCount">消费失败最大重试次数</param>
    /// <returns></returns>
    string Consume(string queueName, Func<string, bool> consumeFunc, int maxRetryCount = 3);
    /// <summary>
    /// 消费消息
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="queueName"></param>
    /// <param name="consumeFunc"></param>
    /// <param name="maxRetryCount"></param>
    /// <returns></returns>
    string Consume<T>(string queueName, Func<T, bool> consumeFunc, int maxRetryCount = 3);
}
七、ABP.Redis缓存
依赖包:已安装
Install-Package StackExchange.Redis -Version 2.6.104
配置:放入appsettings.json文件里
{
    "Redis": {
        "ConnectionString": "Redis数据库连接字符串",
        "DefaultDatabase": 0,//默认使用的数据库,不指定为0
        "Enable": true,//是否启用,如果不启用会用内存缓存
        "LockType": 1,//锁类型:1=线程锁,2=Redis分布式锁。默认值=1
    }
}
初始化:
1. 注册ABP依赖模块:AbpRedisModule
缓存使用说明:
1. 引入依赖注入实例ICacheService
/// <summary>
/// 缓存服务
/// </summary>
public interface ICacheService
{
    /// <summary>
    /// 添加对象
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <param name="expiry">过期时间</param>
    /// <param name="dbIndex">数据库索引,如果为null则使用配置文件里的DefaultDatabase</param>
    /// <returns></returns>
    bool Add<T>(string key, T value, TimeSpan? expiry = null, int? dbIndex = null);
    /// <summary>
    /// 添加字符串
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <param name="expiry"></param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    bool AddString(string key, string value, TimeSpan? expiry = null, int? dbIndex = null);
    /// <summary>
    /// 获取对象
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    T Get<T>(string key, int? dbIndex = null);
    /// <summary>
    /// 获取字符串
    /// </summary>
    /// <param name="key"></param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    string GetString(string key, int? dbIndex = null);
    /// <summary>
    /// 移除缓存
    /// </summary>
    /// <param name="key"></param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    bool Remove(string key, int? dbIndex = null);
    /// <summary>
    /// 搜索key
    /// </summary>
    /// <param name="pattern">搜索关键字</param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    IEnumerable<string> GetAllKey(string pattern, int? dbIndex = null);
    /// <summary>
    /// 缓存是否存在
    /// </summary>
    /// <param name="key"></param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    bool Exists(string key, int? dbIndex = null);
    /// <summary>
    /// 设置缓存过期时间
    /// </summary>
    /// <param name="key"></param>
    /// <param name="expiry"></param>
    /// <param name="dbIndex"></param>
    /// <returns></returns>
    bool KeyExpire(string key, TimeSpan? expiry, int? dbIndex = null);
}
锁使用说明:
1. 引入依赖注入实例ILockService
/// <summary>
/// 锁
/// </summary>
public interface ILockService
{
    /// <summary>
    /// 加锁:回调函数中的代码会串行
    /// </summary>
    /// <param name="lockKey">锁的名称:相同的名称视为同一个锁</param>
    /// <param name="action">回调函数</param>
    /// <param name="secondsTimeout">超时时间:默认5分钟</param>
    T ActionLock<T>(string lockKey, Func<T> action, int secondsTimeout = 300);
    Task<T> ActionLockAsync<T>(string lockKey, Func<Task<T>> action, int secondsTimeout = 300);
}
2. 使用案例
var result = lockService.ActionLock("锁的名称:相同的名称视为同一个锁", () =>
{
    //业务代码执行中
    Thread.Sleep(5000);
    return true;
});
自增使用说明:
1. 引入依赖注入实例IAutoIncrementService
/// <summary>
/// 自增
/// </summary>
public interface IAutoIncrementService
{
    /// <summary>
    /// 生成自增ID
    /// </summary>
    /// <param name="key"></param>
    /// <param name="dbIndex"></param>
    /// <param name="secondsTimeout">超时时间:默认3秒</param>
    /// <returns></returns>
    Task<long> GenerateIdAsync(string key, int? dbIndex = null, int secondsTimeout = 3);
}
八、ABP.SqlSugar数据库访问
依赖包:已安装
Install-Package SqlSugarCore -Version 5.1.4.69
配置:放入appsettings.json文件里
{
    "SqlSugar": {
        "ConnConfigList": [
            {
                "Tenant": "租户名称",
                "ConnStr": "数据库连接字符串",
                "DbType": "数据库类型:0=MySql,1=SqlServer,2=Sqlite,默认值为1",
                "IsAutoCloseConnection": "是否自动关闭连接:默认是",
                "IsEnableReadWriteSeparation": "是否启用读写分离:默认不启用",
                "SlaveConnConfigs":[
                    {
                        "HitRate": 权重:数值越高,读到的概率越高,
                        "ConnStr": "数据库连接字符串"
                    }
                ]//从库连接配置集合,用于读写分离
            }
        ],//数据库连接配置集合
        "CommandTimeOut": "执行命令超时时间:默认30秒",
        "IsWithNoLockQuery": "是否脏读:默认值true",
        "IsUtcNow": "是否是Utc时间:默认值false",
        "WorkId":"用于生成雪花ID的:不同的机器不要配置一样的,默认值1"
    }
}
初始化:
1. 注册ABP依赖模块:AbpSqlSugarModule,无需二级缓存的注册AbpSqlSugarNoCacheModule
使用说明:
1.引入依赖注入实例:ISqlSugarService
/// <summary>
/// 数据库访问服务
/// </summary>
public interface ISqlSugarService
{
    /// <summary>
    /// 数据库配置参数
    /// </summary>
    SqlSugarOptions Options { get; }
    /// <summary>
    /// 数据库访问客户端
    /// </summary>
    SqlSugarClient Client { get; }
    /// <summary>
    /// 获取雪花ID
    /// </summary>
    /// <returns></returns>
    long GetSnowFlakeSingle();
}
2.多库实现:
2.1 需要在Do上面加上属性标注:[Tenant("租户名称,对应配置文件里的Tenant,根据值去匹配数据库连接字符串")]
2.2 增删改查需要使用带WithAttr后缀的方法:例如:QueryableWithAttr、InsertableWithAttr
3.读写分离:主库负责增删改,从库负责读
3.1 启用读写分离的三种方式:默认是不启用的
3.1.1 修改配置文件IsEnableReadWriteSeparation=true(不推荐使用,会影响到所有的代码)
3.1.2 在需要的接口上面添加属性标记[ReadWriteSeparation](推荐使用,影响范围只有这个接口)
3.1.3 用SlaveQueryableWithAttr方法查询数据(合理使用,对于时效性要求不高的查询可以用)
3.2 事务里会强制读主库,如需在事务外读主库用MasterQueryableWithAttr方法查询数据。
仓储使用说明:
应用服务层代码:
public class CfgAppImpl : RepositoryService<CfgAppDo>, IScopedDependency
{
    public CfgAppImpl(ISqlSugarService db, IObjectMapper mapper) : base(db, mapper)
    {
    }
}
展现层调用代码:
public class CfgAppController : ControllerBase
{
    private readonly CfgAppImpl cfgAppImpl;
    public CfgAppController(CfgAppImpl cfgAppImpl)
    {
        this.cfgAppImpl = cfgAppImpl;
    }
    public async Task Add()
    {
        AddOrEditCfgAppRequestDto dto = new AddOrEditCfgAppRequestDto()
        {
            CfgId = 10003,
            Name = "Name3",
            Value = "Value3"
        };
        var result = await cfgAppImpl.InsertAsync(dto);
    }
    public async Task Edit()
    {
        AddOrEditCfgAppRequestDto dto = new AddOrEditCfgAppRequestDto()
        {
            CfgId = 10001,
            Name = "Name11",
            Value = "Value11"
        };
        var result = await cfgAppImpl.UpdateAsync(dto);
    }
    public async Task Del()
    {
        var result = await cfgAppImpl.DeleteAsync(new CfgAppDo() { CfgId = 10001 });
    }
    public async Task ToPageList()
    {
        var result = await cfgAppImpl.ToPageListAsync(new QueryParameters(), 1, 2);
    }
}
分表使用说明:
[SplitTable(SplitType.Month)]
[SugarTable("SplitTestTable_{year}{month}{day}"), Tenant("EDM")]
public class SplitTestTableDo
{
    [SugarColumn(IsPrimaryKey = true)]
    public long Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    [SplitField] //分表字段 在插入的时候会根据这个字段插入哪个表,在更新删除的时候用这个字段找出相关表
    public DateTime CreateTime { get; set; }
}
增:
//已CreateTime字段按月分表
List<SplitTestTableDo> list = new List<SplitTestTableDo>()
{
    new SplitTestTableDo(){ Id = 1, Name = "Name1", Age = 100, CreateTime = DateTime.Parse("2024-1-3 10:10:10")},
    new SplitTestTableDo(){ Id = 2, Name = "Name2", Age = 200, CreateTime = DateTime.Parse("2024-2-15 11:11:11")},
    new SplitTestTableDo(){ Id = 3, Name = "Name3", Age = 300, CreateTime = DateTime.Parse("2024-2-16 12:12:12")},
    new SplitTestTableDo(){ Id = 4, Name = "Name4", Age = 400, CreateTime = DateTime.Parse("2024-3-15")}
};
var result = db.Client.InsertableWithAttr<SplitTestTableDo>(list).SplitTable().ExecuteCommand();
删:
long id = 3;
DateTime createTime = DateTime.Parse("2024-2-1");
var result = db.Client.DeleteableWithAttr<SplitTestTableDo>()
    .Where(o => o.Id == id)
    //.SplitTable(tas => tas)//不知道该条数据的创建时间是哪个月(分表字段),此写法会把1-3月的表都删除一遍
    .SplitTable(tas =>
    {
        return tas.Where(o => o.Date == createTime);
    })//知道该条数据的创建时间是哪个月会只删除2月的表(SplitTestTable_20240201)
    .ExecuteCommand();
改:
long id = 2;
DateTime createTime = DateTime.Parse("2024-2-1");
var result = db.Client.UpdateableWithAttr<SplitTestTableDo>()
    .SetColumns(o => o.Age == 201)
    .Where(o => o.Id == id)
    //.SplitTable(tas => tas)//不知道该条数据的创建时间是哪个月(分表字段),此写法会把1-3月的表都修改一遍
    .SplitTable(tas =>
    {
        return tas.Where(o => o.Date == createTime);
    })//知道该条数据的创建时间是哪个月会只修改2月的表(SplitTestTable_20240201)
    .ExecuteCommand();
查:
DateTime createTime = DateTime.Parse("2024-1-1");
DateTime startTime = DateTime.Parse("2024-2-3");
DateTime endTime = DateTime.Parse("2024-3-25");
var result = db.Client.QueryableWithAttr<SplitTestTableDo>()
    .Where(o => o.Age >= 200 && o.Age <= 500)
    //.SplitTable()//此写法会把1-3月的表并起来查
    //.SplitTable(startTime, endTime)//此写法会把2-3月的表并起来查,查询2月3号-3月25号的数据
    .SplitTable(tas =>
    {
        return tas.Where(o => o.Date == createTime);
    })//此写法只查1月份的数据
    .ToList();
九、ABP.Swagger接口文档
依赖包:已安装
Install-Package Swashbuckle.AspNetCore -Version 6.5.0
Install-Package Swashbuckle.AspNetCore.Annotations -Version 6.5.0
初始化:
1. 注册ABP依赖模块:AbpSwaggerModule
使用说明:
1. 在浏览器中输入路由地址:/swagger即可查看接口文档
十、ABP.Kafka消息队列
依赖包:已安装
Install-Package Confluent.Kafka -Version 1.9.0
Install-Package protobuf-net -Version 3.1.17
配置:放入appsettings.json文件里
{
    "Kafka": {
        "ProduceBootstrapServers": "生产者 kafka 代理主机:端口",
        "ConsumeBootstrapServers": "消费者 kakfa 代理主机:端口",
        "UserName":"认证账号",
        "Password":"认证密码"
    }
}
初始化:
1. 注册ABP依赖模块:AbpKafkaModule
使用说明:
1. 拥入依赖注入实例:ICommonConfulentKafka
public interface ICommonConfulentKafka
{
    /// <summary>
    /// 获取主题分区数
    /// </summary>
    /// <param name="topicName">主题</param>
    /// <returns></returns>
    int? GetPartitionsByTopic(string topicName);
    /// <summary>
    /// 删除标题
    /// </summary>
    /// <param name="topicName">标题名称</param>
    /// <returns></returns>
    Task DeleteTopicAsync(string topicName);
    /// <summary>
    /// 创建标题
    /// </summary>
    /// <param name="topicName">标题名称</param>
    /// <returns></returns>
    Task CreateTopicAsync(string topicName);
    /// <summary>
    /// 生产者 发送消息
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <param name="topicName">标题</param>
    /// <param name="msg">消息内容</param>
    /// <returns></returns>
    Task  ProduceMsgAsync<T>(string topicName, T msg);
    /// <summary>
    /// 消费者  接收消息
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <param name="topics">标题类型</param>
    /// <param name="group">组名</param>
    /// <returns></returns>
    IConsumer<Ignore, T> ConsumeMsg<T>(string group);
    /// <summary>
    /// 生产者 发送Proto消息
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <param name="topicName">标题</param>
    /// <param name="msg">消息内容</param>
    /// <returns></returns>
    Task ProduceProtoBufMsgAsync<T>(string topicName, T msg);
    /// <summary>
    /// 消费者  接收Proto消息
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <param name="topics">标题类型</param>
    /// <param name="group">组名</param>
    /// <returns></returns>
    IConsumer<Ignore, T> ConsumeProtoBufMsg<T>(string group);
}
十一、ABP.Mail邮件服务
配置:放入appsettings.json文件里
{
    "Mail": {
        "Host": "邮件服务商主机地址",
        "Port": "邮件服务商主机端口号",
        "SenderEmail": "发件人邮箱",
        "SenderPwd": "发件人密码",
        "SenderName": "发件人名称",
        "WriteBackEmail": "回信邮箱"
    }
}
初始化:
1. 注册ABP依赖模块:AbpMailModule
使用说明:
1.引入依赖注入实例:IMailService
/// <summary>
/// 邮件服务
/// </summary>
public interface IMailService
{
    /// <summary>
    /// 发送邮件
    /// </summary>
    /// <param name="sendMailOptions"></param>
    void Send(SendMailOptions sendMailOptions);
}
发送邮件参数说明SendMailOptions:
AddresseeEmail:收件人邮箱。
Subject:主题。
Contents:内容集合
    ContentType:内容类型text/html|text/plain|text/richtext|text/xml
    更多类型查看MediaTypeNames常量。
    Content:内容文本。
    ContentEncoding:内容编码。
Attachments:附件集合。
十二、ABP.SMS短信服务
依赖包:已安装
阿里云短信服务开发包
Install-Package AlibabaCloud.SDK.Dysmsapi20170525 -Version 2.0.23
配置:放入appsettings.json文件里
{
    "SmsOptions": {
        "AccessKeyId": "账号ID",
        "AccessKeySecret": "api访问密钥"
    }//短信平台参数
}
初始化:
1. 注册ABP依赖模块:AbpSmsModule
使用说明:
1.引入依赖注入实例:ISmsService
/// <summary>
/// 短信服务
/// </summary>
public interface ISmsService
{
    /// <summary>
    /// 发送短信
    /// </summary>
    /// <param name="sendSmsOptions"></param>
    /// <returns></returns>
    bool Send(SendSmsOptions sendSmsOptions);
}
发送短信参数说明SendSmsOptions:
PhoneNumbers:接收人手机号码。
SignName:签名,显示在短信前面,需要去短信平台申请。
TemplateCode:模板CODE,需要去短信平台申请。
TemplateParam:模板参数:json对象字符串。
十三、ABP.FileCore文件服务
使用场景:
保存文件、excel导入导出
依赖包:已安装
Install-Package NPOI -Version 2.6.0
初始化:
1. 注册ABP依赖模块:AbpFileCoreModule
2. 在配置服务容器中添加以下代码
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    services.AddFileCore(options =>
    {
        options.AllowFileExts = "允许的文件扩展名集合";
        options.LimitMaxSize = "文件最大长度:单位字节";
        options.StorageRootDir = "存储文件根目录";
        options.MappingUrlPath = "访问文件url地址根路径";
    });
}
属性说明:
导入Excel:
导入列*:[Display(Name = "列名")]
忽略导入列:[IgnoreImportColumn]
必填列:[Required(ErrorMessage = "{0}不能为空")],更多属性参考netcore数据注解模型验证
导出Excel:
导出列*:[Display(Name = "列名", Order = 排序:非必填,不填按属性从上到下排)]
忽略导出列:[IgnoreColumn]
行高:[RowHeight(表头行高, 数据列行高)]
列宽:[HeaderStyle(ColumnSize = 单位:字符个数)]
表头字体颜色:[HeaderFont(Color = HSSFColor.Pink.Index)]
数据列字体颜色:[DataFont(Color = HSSFColor.Red.Index)]
数据列样式:[DataStyle(DataFormat = "yyyy-MM-dd HH:mm")]
数据列求和:[ColumnStats((int)FunctionEnum.Sum)]
数据列求平均值:[ColumnStats((int)FunctionEnum.Avg, OffsetRow = 3)]
使用说明:
1.引入依赖注入实例:IFileService
/// <summary>
/// 文件服务
/// </summary>
public interface IFileService
{
    /// <summary>
    /// 保存文件
    /// </summary>
    /// <param name="fileData">文件数据</param>
    /// <param name="fileName">文件名称</param>
    /// <param name="fileRelativeDir">文件的相对目录,会跟存储文件的根目录拼接起来</param>
    /// <returns></returns>
    SaveFileResultDto SaveFile(byte[] fileData, string fileName, string fileRelativeDir = "");
}
保存文件返回值说明SaveFileResultDto:
FileDir:文件完整存储目录
Url:访问文件的url地址
2.引入依赖注入实例:IExcelService
/// <summary>
/// Excel导入导出
/// </summary>
public interface IExcelService
{
    /// <summary>
    /// 导出
    /// </summary>
    /// <typeparam name="TExportDto"></typeparam>
    /// <param name="data">数据集合</param>
    /// <param name="fileName">文件名称</param>
    /// <param name="fileRelativeDir">文件的相对目录,如果传了会跟存储文件的根目录拼接起来</param>
    /// <returns></returns>
    Task<ExportFileResultDto> ExportAsync<TExportDto>(List<TExportDto> data, string fileName, string fileRelativeDir = "") where TExportDto : class, new();
    /// <summary>
    /// 导出:支持复杂表头合并、动态表头
    /// </summary>
    /// <param name="data">导出信息</param>
    /// <param name="fileName">文件名称</param>
    /// <param name="fileRelativeDir">文件的相对目录,如果传了会跟存储文件的根目录拼接起来</param>
    /// <param name="isUnique"></param>
    /// <returns></returns>
    Task<ExportFileResultDto> ExportAsync(ExportInfoDto data, string fileName, string fileRelativeDir = "Export");
    /// <summary>
    /// 导入
    /// </summary>
    /// <typeparam name="TImportDto"></typeparam>
    /// <param name="filePath">导入的文件路径</param>
    /// <param name="importFunc">导入成功回调函数</param>
    /// <param name="fileName">文件名称</param>
    /// <param name="fileRelativeDir">文件的相对目录,会跟存储文件的根目录拼接起来</param>
    /// <returns></returns>
    Task<ImportFileResultDto> ImportAsync<TImportDto>(string filePath, Func<IEnumerable<TImportDto>, IEnumerable<TImportDto>> importFunc, string fileName, string fileRelativeDir = "") where TImportDto : ImportItemDto, new();
}
导出返回值说明:ExportFileResultDto:
FileDir:文件完整存储目录
FileData:文件数据:字节数组
Url:访问文件的url地址
导入返回值说明:ImportFileResultDto:
FileDir:导入结果文件完整存储目录
FileData:导入结果文件数据:字节数组
Url:导入结果文件的url地址
Total:导入总记录数
SuccessCount:导入成功记录数
FailCount:导入失败记录数
导入输入参数说明:ExportInfoDto
SheetName:表名
HeaderList:表头集合
DataList:表体集合
AutoMerge:自动合并表头
使用案例:导出复杂表头合并、动态表头
合并前: <table border="1"> <tr> <td>序号1</td> <td>单位名称</td> <td>合计</td> <td>合计</td> <td>合计</td> <td>动态列1</td> <td>动态列2</td> </tr> <tr> <td>序号2</td> <td>单位名称</td> <td>合计A</td> <td>合计B</td> <td>合计C</td> <td>动态列1</td> <td>动态列2</td> </tr> <tr> <td>序号3</td> <td>单位信息</td> <td>合计A</td> <td>合计B</td> <td>合计C</td> <td>动态列1</td> <td>动态列2</td> </tr> </table> 合并后: <table border="1"> <tr> <td>序号1</td> <td rowspan="2">单位名称</td> <td colspan="3" align="center">合计</td> <td rowspan="3">动态列1</td> <td rowspan="3">动态列2</td> </tr> <tr> <td>序号2</td> <td rowspan="2">合计A</td> <td rowspan="2">合计B</td> <td rowspan="2">合计C</td> </tr> <tr> <td>序号3</td> <td>单位信息</td> </tr> </table>
1.导出:支持复杂表头合并、动态表头
//表头集合
var headerList = new List<List<CellInfo>>
{
    new List<CellInfo> 
    {
        new CellInfo()
        {
            Name = "序号1",//列名
            HorizontalAlignment = HorizontalAlignment.Center,//水平对齐
            VerticalAlignment = VerticalAlignment.Center,//垂直对齐
            FontColor = IndexedColors.Black.Index,//字体颜色
            FontHeightInPoints = 11,//字体大小
            FillForegroundColor = IndexedColors.White.Index,//填充背景颜色
            FillPattern = FillPattern.NoFill//填充模式
        },
        new CellInfo(){Name = "单位名称"},
        new CellInfo(){Name = "合计"},
        new CellInfo(){Name = "合计"},
        new CellInfo(){Name = "合计"},
        new CellInfo(){Name = "动态列1"},
        new CellInfo(){Name = "动态列2"}
    },
    new List<CellInfo>
    {
        new CellInfo(){Name = "序号2"},
        new CellInfo(){Name = "单位名称"},
        new CellInfo(){Name = "合计A"},
        new CellInfo(){Name = "合计B"},
        new CellInfo(){Name = "合计C"},
        new CellInfo(){Name = "动态列1"},
        new CellInfo(){Name = "动态列2"}
    },
    new List<CellInfo>
    {
        new CellInfo(){Name = "序号3"},
        new CellInfo(){Name = "单位信息"},
        new CellInfo(){Name = "合计A"},
        new CellInfo(){Name = "合计B"},
        new CellInfo(){Name = "合计C"},
        new CellInfo(){Name = "动态列1"},
        new CellInfo(){Name = "动态列2"}
    }
};
//表体集合
var dataList = new DataTable();
dataList.Columns.Add("column1", typeof(int));//列名可以不跟上面列名保持一致,但是顺序必须保持一致
dataList.Columns.Add("单位名称", typeof(string));
dataList.Columns.Add("合计A", typeof(int));
dataList.Columns.Add("合计B", typeof(int));
dataList.Columns.Add("合计C", typeof(int));
dataList.Columns.Add("动态列1", typeof(int));
dataList.Columns.Add("动态列2", typeof(int));
//添加测试数据
dataList.Rows.Add(1, "单位111", 86, 13, 45, 11, 12);
dataList.Rows.Add(2, "单位222", 23, 453, 234, 21, 22);
//导出
var exportInfo = new ExportInfoDto()
{
    SheetName = "复杂表头合并、动态表头",
    HeaderList = headerList,
    DataList = dataList,
    AutoMerge = true//自动合并表头
};
var result = await excelService.ExportAsync(exportInfo, "导出的Excel.xlsx");
十四、ABP.PaymentCore在线支付
使用场景:
用于空中云汇支付、支付宝支付
依赖包:已安装
Install-Package RestSharp -Version 106.13.0
初始化:
1. 注册ABP依赖模块:AbpPaymentCoreModule
2. 在配置服务容器中添加以下代码
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    //添加空中云汇支付
    services.AddAirwallexPayment(options =>
    {
        options.ClientId = "客户ID:第三方支付平台提供";
        options.ApiKey = "调用Api密钥:第三方支付平台提供";
        options.GetTokenUrl = "获取Token接口地址";
        options.PaymentUrl = "支付下单接口地址";
        options.IsRecordRequestMessage = "是否记录请求报文:默认值为true";
        options.CacheKeyPrefix = "缓存key前缀:默认值为Airwallex_";
    });
}
使用说明:
1.引入依赖注入实例:IPaymentService
/// <summary>
/// 支付服务
/// </summary>
public interface IPaymentService
{
    /// <summary>
    /// 开始支付:向第三方支付平台下单
    /// </summary>
    /// <param name="request"></param>
    /// <param name="paymentType"></param>
    /// <returns></returns>
    DirectPayResponse BeginPay(DirectPayRequest request, PaymentTypeEnum paymentType);
    /// <summary>
    /// 支付完成:解析第三方平台支付成功信息
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    Task<DirectPayResult> PayComplete(HttpContext context);
    /// <summary>
    /// 支付完成
    /// </summary>
    /// <param name="packet"></param>
    /// <returns></returns>
    DirectPayResult PayComplete(HttpPacket packet);
}
1.支付下单参数说明DirectPayRequest:
TradeId:唯一交易ID
TradeName:订单编号
Amount:支付金额
Currency:支付币种
2.支付下单返回值说明DirectPayResponse:
Type:数据类型:由于各平台的支付方式不一样,返回的数据类型也不一样,有的是返回支付链接、有的是返回支付密钥,还有返回支付二维码的
UrlData:支付链接
JsonData:支付密钥
ImageData:二维码数据:字节数组
3.支付成功返回值说明:DirectPayResult
Success:是否支付成功
TradeId:唯一交易ID:我们支付下单的时候提供的
TradeName:订单编号:我们支付下单的时候提供的
PaymentType:支付类型
ThirdPartyTradeId:第三方交易ID
PracticalCurrency:实付币种
PracticalAmount:实付金额
十五、ABP.AutoInject自动依赖注入
使用场景:
1.用于属性依赖注入懒加载,首次访问属性才去容器中创建实例
2.用于依赖注入检查:避免忘记做依赖注入了,导致运行中才报空引用错误!
依赖包:已安装
Install-Package Volo.Abp.Autofac -Version 4.4.4
Install-Package Volo.Abp.AspNetCore -Version 4.4.4
初始化:
1.在Startup启动类加上下面方法
public void ConfigureContainer(ContainerBuilder builder)
{
    //添加自动注入:给依赖注入实例做动态代理,动态代理会通过拦截器来拦截属性,从而实现属性自动懒加载,只在访问属性的时候才去获取依赖注入实例
    builder.AddAutoInject<AppModule>();
}
2. 在配置服务容器中添加以下代码
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    //用于将控制器注入到容器中,以便对控制器做动态代理
    services.AddControllers().AddControllersAsServices();
}
public override void PostConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    //依赖注入检查:避免忘记做依赖注入了,导致运行中才报空引用错误!
    services.DependencyInjectCheck<AppModule>();
}
使用说明:
1.在Applidation应用服务层使用案例
public class TestC : IScopedDependency, IAutoLazyInject
{
    /// <summary>
    /// 属性懒加载:自动获取依赖注入实例
    /// 要求:
    /// 1.类必须继承IAutoLazyInject接口
    /// 2.属性必须是虚属性
    /// 3.属性必须是public的
    /// 4.属性必须加[AutoLazyInject]标记
    /// 建议:
    /// 1.建议用下划线开头,表示私有,不提倡在类的外部去访问
    /// </summary>
    [AutoLazyInject]
    public virtual TestB _testB { get; }
    public void Test()
    {
        _testB.Test();
    }
}
2.在Api展现层使用案例
跟应用服务层一样,控制器继承IAutoLazyInject接口
十六、ABP.RateLimit接口IP白名单、IP限流
使用场景:
用于接口限流、白名单访问
接口限流github:https://github.com/stefanprodan/AspNetCoreRateLimit
依赖包:已安装
Install-Package AspNetCoreRateLimit -Version 4.0.2
配置:放入appsettings.json文件里
{
    "RateLimit": {
        "IpAccess": {
          //是否启用IP白名单访问
          "EnableIpWhitelistAccess": true,
          //允许访问的IP白名单
          "AccessIpWhitelist": [ "::1" ],
          //是否启用代理服务
          "EnableProxyServer": false,
          //允许访问的代理服务器IP白名单
          "ProxyServerIpWhitelist": [],
          //如果使用代理服务器需要填写,将从该请求头部获取客户端IP地址
          "RealIpHeader": null
        },
        "IpRateLimiting": {
          //false:则全局将应用限制,并且仅应用具有作为端点的规则*。例如,如果您设置每秒5次调用的限制,则对任何端点的任何HTTP调用都将计入该限制
          //true:则限制将应用于每个端点,如{HTTP_Verb}{PATH}。例如,如果您为*:/api/values客户端设置每秒5个呼叫的限制,
          "EnableEndpointRateLimiting": false,
          //false:拒绝的API调用不会添加到调用次数计数器上;如 客户端每秒发出3个请求并且您设置了每秒一个调用的限制,则每分钟或每天计数器等其他限制将仅记录第一个调用,即成功的API调用。
          //true:如果您希望被拒绝的API调用计入其他时间的显示(分钟,小时等),则必须设置StackBlockedRequests为true。
          "StackBlockedRequests": false,
          //应用服务器背后是一个反向代理,如果你的代理服务器使用不同的页眉然后提取客户端IP X-Real-IP使用此选项来设置
          "RealIpHeader": "X-Real-IP",
          //限制状态码
          "QuotaExceededResponse": {
            "Content": "{{\"Success\":false,\"ResultCode\":400,\"Message\":\"Quota exceeded. Maximum allowed: {0} per {1}. Please try again in {2} second(s).\"}}",
            "ContentType": "application/json",
            "StatusCode": 429
          },
          //IP白名单:支持Ip v4和v6 
          //"IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ],
          //端点白名单
          //"EndpointWhitelist": [ "get:/api/license", "*:/api/status" ],
          //通用规则
          "GeneralRules": [
            {
              //端点路径
              "Endpoint": "*",
              //时间段,格式:{数字}{单位};可使用单位秒分时天:s, m, h, d
              "Period": "5s",
              //限制
              "Limit": 2
            }
          ]
        },
        "IpRateLimitPolicies": {
          //ip规则
          "IpRules": [
            {
              "Ip": "::1",
              "Rules": [
                {
                  "Endpoint": "*",
                  "Period": "5s",
                  "Limit": 3
                }
              ]
            }
          ]
        }
    }
}
初始化:
1. 注册ABP依赖模块:AbpIpWhitelistModule、AbpIpRateLimitModule
[DependsOn(typeof(AbpIpWhitelistModule), typeof(AbpIpRateLimitModule))]
public class AbpTestModule:AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var services = context.Services;
    }
}
2. 如需单独对某个ip设置限流,需要加上以下代码
public static async Task Main(string[] args)
{
    //CreateHostBuilder(args).Build().Run();
    var webHost = CreateHostBuilder(args).Build();
    using (var scope = webHost.Services.CreateScope())
    {
        // get the IpPolicyStore instance
        var ipPolicyStore = scope.ServiceProvider.GetRequiredService<IIpPolicyStore>();
        // seed IP data from appsettings
        await ipPolicyStore.SeedAsync();
    }
    await webHost.RunAsync();
}
| Product | Versions Compatible and additional computed target framework versions. | 
|---|---|
| .NET | net5.0 is compatible. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. | 
- 
                                                    net5.0- AlibabaCloud.SDK.Dysmsapi20170525 (>= 2.0.23)
- AlipaySDKNet.Standard (>= 4.7.295)
- AspNetCoreRateLimit (>= 4.0.2)
- CCITU.Common (>= 2.1.4)
- Confluent.Kafka (>= 1.9.0)
- EasyCaching.InMemory (>= 1.9.2)
- EasyCaching.Redis (>= 1.9.2)
- EasyCaching.Serialization.Json (>= 1.9.2)
- EPPlus (>= 6.0.3)
- Hangfire (>= 1.8.1)
- Hangfire.Dashboard.BasicAuthorization (>= 1.0.2)
- Hangfire.HttpJob (>= 3.7.6)
- Hangfire.HttpJob.Client (>= 1.2.9)
- Hangfire.Redis.StackExchange (>= 1.8.7)
- JWT (>= 10.0.2)
- LogDashboard (>= 1.4.8)
- NPOI (>= 2.6.0)
- Portable.BouncyCastle (>= 1.9.0)
- protobuf-net (>= 3.1.17)
- RestSharp (>= 106.13.0)
- Serilog.AspNetCore (>= 8.0.0)
- SqlSugarCore (>= 5.1.4.154)
- StackExchange.Redis (>= 2.6.111)
- Swashbuckle.AspNetCore (>= 6.5.0)
- Swashbuckle.AspNetCore.Annotations (>= 6.5.0)
- Volo.Abp.AspNetCore (>= 4.4.4)
- Volo.Abp.AspNetCore.Mvc (>= 4.4.4)
- Volo.Abp.Autofac (>= 4.4.4)
- Volo.Abp.AutoMapper (>= 4.4.4)
 
- 
                                                    net6.0- AlibabaCloud.SDK.Dysmsapi20170525 (>= 2.0.23)
- AlipaySDKNet.Standard (>= 4.7.295)
- AspNetCoreRateLimit (>= 4.0.2)
- CCITU.Common (>= 2.1.4)
- Confluent.Kafka (>= 2.4.0)
- EPPlus (>= 6.0.3)
- Hangfire (>= 1.8.1)
- Hangfire.Dashboard.BasicAuthorization (>= 1.0.2)
- Hangfire.HttpJob (>= 3.7.6)
- Hangfire.HttpJob.Client (>= 1.2.9)
- Hangfire.Redis.StackExchange (>= 1.8.7)
- JWT (>= 10.0.2)
- LogDashboard (>= 1.4.8)
- NPOI (>= 2.6.0)
- Portable.BouncyCastle (>= 1.9.0)
- protobuf-net (>= 3.2.30)
- RestSharp (>= 106.13.0)
- Serilog.AspNetCore (>= 8.0.0)
- SqlSugarCore (>= 5.1.4.154)
- StackExchange.Redis (>= 2.6.111)
- Swashbuckle.AspNetCore (>= 6.5.0)
- Swashbuckle.AspNetCore.Annotations (>= 6.5.0)
- Volo.Abp.AspNetCore (>= 6.0.3)
- Volo.Abp.AspNetCore.Mvc (>= 6.0.3)
- Volo.Abp.Autofac (>= 6.0.3)
- Volo.Abp.AutoMapper (>= 6.0.3)
 
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | 
|---|---|---|
| 2.2.12 | 221 | 10/10/2025 | 
| 2.2.11 | 213 | 9/2/2025 | 
| 2.2.10 | 176 | 9/2/2025 | 
| 2.2.9 | 163 | 9/2/2025 | 
| 2.2.8 | 527 | 3/24/2025 | 
| 2.2.7 | 212 | 3/20/2025 | 
| 2.2.6 | 228 | 11/8/2024 | 
| 2.2.5 | 246 | 10/23/2024 | 
| 2.2.4 | 186 | 10/14/2024 | 
| 2.2.3 | 206 | 9/20/2024 | 
| 2.2.2 | 193 | 9/19/2024 | 
| 2.2.1 | 183 | 9/19/2024 | 
| 2.2.0 | 216 | 9/19/2024 | 
| 2.1.34 | 208 | 9/19/2024 | 
| 2.1.33 | 244 | 6/27/2024 | 
| 2.1.32 | 163 | 6/3/2024 | 
| 2.1.31 | 237 | 5/29/2024 | 
| 2.1.30 | 194 | 5/29/2024 | 
| 2.1.29 | 207 | 5/29/2024 | 
| 2.1.28 | 200 | 5/28/2024 | 
| 2.1.27.2 | 200 | 5/16/2024 | 
| 2.1.27.1 | 160 | 5/13/2024 | 
| 2.1.27 | 145 | 5/13/2024 | 
| 2.1.26 | 328 | 2/23/2024 | 
| 2.1.25 | 196 | 2/5/2024 | 
| 2.1.25-beta | 181 | 2/5/2024 | 
| 2.1.24 | 296 | 2/5/2024 | 
| 2.1.23 | 218 | 1/24/2024 | 
| 2.1.22 | 294 | 1/9/2024 | 
| 2.1.21 | 199 | 1/8/2024 | 
| 2.1.20 | 225 | 12/29/2023 | 
| 2.1.19 | 246 | 12/1/2023 | 
| 2.1.18 | 256 | 11/30/2023 | 
| 2.1.17 | 204 | 11/23/2023 | 
| 2.1.16 | 305 | 11/23/2023 | 
| 2.1.15 | 230 | 11/21/2023 | 
| 2.1.14 | 185 | 11/17/2023 | 
| 2.1.13 | 225 | 11/16/2023 | 
| 2.1.12 | 224 | 11/13/2023 | 
| 2.1.11 | 206 | 11/2/2023 | 
| 2.1.10 | 217 | 10/18/2023 | 
| 2.1.9 | 201 | 9/20/2023 | 
| 2.1.8 | 202 | 9/15/2023 | 
| 2.1.7 | 229 | 9/11/2023 | 
| 2.1.6 | 265 | 9/5/2023 | 
| 2.1.5 | 264 | 8/26/2023 | 
| 2.1.4 | 233 | 8/22/2023 | 
| 2.1.3 | 216 | 8/16/2023 | 
| 2.1.2 | 237 | 8/15/2023 | 
| 2.1.1 | 287 | 8/7/2023 | 
| 2.1.0 | 265 | 8/4/2023 | 
| 2.0.20 | 267 | 8/3/2023 | 
| 2.0.19 | 259 | 8/3/2023 | 
| 2.0.18 | 250 | 7/29/2023 | 
| 2.0.17 | 254 | 7/29/2023 | 
| 2.0.16 | 261 | 7/13/2023 | 
| 2.0.15 | 270 | 7/6/2023 | 
| 2.0.14 | 256 | 7/5/2023 | 
| 2.0.13 | 251 | 6/29/2023 | 
| 2.0.12 | 232 | 6/28/2023 | 
| 2.0.11 | 281 | 6/27/2023 | 
| 2.0.10 | 259 | 6/25/2023 | 
| 2.0.9 | 294 | 6/20/2023 | 
| 2.0.8 | 272 | 6/19/2023 | 
| 2.0.7 | 289 | 6/19/2023 | 
| 2.0.6 | 278 | 6/13/2023 | 
| 2.0.5 | 233 | 6/13/2023 | 
| 2.0.4 | 274 | 6/12/2023 | 
| 2.0.3 | 245 | 6/12/2023 | 
| 2.0.2 | 266 | 6/9/2023 | 
| 2.0.1 | 302 | 6/9/2023 | 
| 2.0.0 | 266 | 6/9/2023 | 
| 1.0.2 | 304 | 6/7/2023 | 
| 1.0.1 | 226 | 6/7/2023 | 
| 1.0.0 | 287 | 6/7/2023 |