RuoVea.OmiApi.Article
10.0.0.6
dotnet add package RuoVea.OmiApi.Article --version 10.0.0.6
NuGet\Install-Package RuoVea.OmiApi.Article -Version 10.0.0.6
<PackageReference Include="RuoVea.OmiApi.Article" Version="10.0.0.6" />
<PackageVersion Include="RuoVea.OmiApi.Article" Version="10.0.0.6" />
<PackageReference Include="RuoVea.OmiApi.Article" />
paket add RuoVea.OmiApi.Article --version 10.0.0.6
#r "nuget: RuoVea.OmiApi.Article, 10.0.0.6"
#:package RuoVea.OmiApi.Article@10.0.0.6
#addin nuget:?package=RuoVea.OmiApi.Article&version=10.0.0.6
#tool nuget:?package=RuoVea.OmiApi.Article&version=10.0.0.6
RuoVea.OmiApi.Article
文章分类和内容管理 API 组件 —— 基于 .NET 构建的轻量级、跨平台内容管理系统后端。
RuoVea.OmiApi.Article 是一个开箱即用的文章管理系统 NuGet 包,提供文章、分类、标签、评论、点赞、收藏、SEO 元数据的完整 CRUD 能力。基于 SqlSugar ORM 和 DynamicWebApi,注册即自动生成 RESTful API 端点,支持 MySql / SqlServer / PostgreSQL / SQLite / Oracle 等多种数据库。
目录
概览
功能特性
| 模块 | 功能 |
|---|---|
| 📄 文章管理 | 增删改查、分页搜索、置顶/精选、草稿/发布/下架状态流转、阅读量统计 |
| 📁 分类管理 | 无限层级树形分类、Code 路由别名、启用/停用开关 |
| 🏷️ 标签管理 | 扁平标签体系、Name/Code 唯一约束、按用户筛选 |
| 💬 评论管理 | 楼层式回复(PId 自引用)、评论审核状态、评论计数联动 |
| ❤️ 点赞/收藏 | 用户级去重、点赞数/收藏数与文章表双向同步事务 |
| 🔍 SEO 管理 | 每篇文章独立 SEO(Title/Keywords/Description/CanonicalUrl),一对一关联 |
| 🗄️ 多库支持 | MySql、SqlServer、PostgreSQL、SQLite、Oracle、Dm 等 |
| 🧩 自动API | 实现 IApplicationService 即自动映射为 REST 控制器 |
架构一览
┌─────────────────────────────────────────────────────┐
│ NuGet Package │
│ RuoVea.OmiApi.Article │
├─────────────────────────────────────────────────────┤
│ Service Layer (8 Services) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Article │ │ Category │ │ Tag │ │
│ │ Service │ │ Service │ │ Service │ │
│ ├───────────┤ ├───────────┤ ├───────────┤ │
│ │ Comment │ │ Favorite │ │ Like │ │
│ │ Service │ │ Service │ │ Service │ │
│ ├───────────┤ ├───────────┤ └───────────┘ │
│ │ArticleTag │ │CatArticle │ │
│ │ Service │ │ Service │ │
│ └───────────┘ └───────────┘ │
├─────────────────────────────────────────────────────┤
│ Domain Layer (9 Entities) │
│ ArtArticle ─── ArtSeo (1:1) │
│ ArtArticle ─── ArtLike / ArtFavorite / ArtComment │
│ ArtArticle ──< ArtArticleTag >── ArtTag │
│ ArtArticle ──< ArtCategoryArticle >── ArtCategory │
│ ArtCategory ─── ArtCategory (Self-ref tree) │
│ ArtComment ─── ArtComment (Self-ref tree) │
├─────────────────────────────────────────────────────┤
│ Infrastructure │
│ SqlSugar ORM · DynamicWebApi · ExSugar Repo │
└─────────────────────────────────────────────────────┘
支持的 .NET 版本
| TFM | NuGet 版本 |
|---|---|
net8.0 |
8.0.0.19 |
net10.0 |
10.0.0.3 |
安装
NuGet 包管理器
# .NET 8 项目
Install-Package RuoVea.OmiApi.Article -Version 8.0.0.19
# .NET 10 项目
Install-Package RuoVea.OmiApi.Article -Version 10.0.0.3
.NET CLI
dotnet add package RuoVea.OmiApi.Article --version 8.0.0.19
依赖项
本包依赖以下组件(安装时会自动引入):
| 包名 | 用途 |
|---|---|
RuoVea.DynamicWebApi |
动态 API 控制器生成 |
RuoVea.ExSugar |
SqlSugar 仓储模式封装 |
30 秒快速开始
1. 配置数据库连接 (appsettings.json)
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./ruovea.db"
}
],
"DbInitConfig": {
"InitTable": true
},
"Swagger": {
"ApiVersions": [
{
"Title": "文章管理",
"Version": "art"
}
]
}
}
支持的 DbType 值:
MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、OpenGauss、ClickHouse等。
2. 注册服务 (Program.cs)
// <summary>
// 在 Program.cs 中注册 OmiApi.Article 组件服务
// </summary>
var builder = WebApplication.CreateBuilder(args);
// 注册动态 Web API(自动将 Service 映射为 REST 控制器)
builder.Services.AddDynamicWebApi(options =>
{
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
// 注册 Article 模块服务(默认 Scoped 生命周期)
builder.Services.AddOmiArticleSetup();
// 注册 SqlSugar ORM
builder.Services.AddSqlSugarSetup();
// 初始化数据库表结构
builder.Services.AddArticleInitSetup();
var app = builder.Build();
app.Run();
3. 启动并访问 Swagger
启动项目后,访问 https://localhost:xxxx/swagger,即可看到 "文章管理" 分组下的全部 RESTful API 端点。
核心场景
场景一:发布一篇文章(含分类、标签、SEO)
// <summary>
// 创建文章 —— 异步写法。包含分类关联、标签关联和 SEO 元数据,在一个事务内完成。
// </summary>
public async Task<bool> CreateArticleAsync(ArticleService articleService)
{
var dto = new ArticleDto
{
Title = "Hello RuoVea",
Content = "<h1>欢迎使用</h1><p>这是文章正文...</p>",
Subtitle = "入门指南",
Summary = "一篇快速上手的教程",
CoverImage = "/images/cover.jpg",
Status = 1, // 1=已发布
IsTop = YesOrNot.N,
IsFeatured = YesOrNot.Y,
Categories = new List<long> { 1, 2 }, // 关联分类 ID
Tags = new List<long> { 3, 4 }, // 关联标签 ID
Seo = new SeoDto
{
MetaTitle = "Hello RuoVea | 入门指南",
MetaKeywords = "RuoVea,Article,入门",
MetaDescription = "RuoVea.OmiApi.Article 快速入门教程",
CanonicalUrl = "/article/hello-ruovea"
}
};
return await articleService.AddDataAsync(dto);
}
// <summary>
// 创建文章 —— 同步写法(通过 .GetAwaiter().GetResult() 调用)
// ⚠️ 注意:在 ASP.NET 上下文中可能导致死锁,仅推荐在 Console/测试环境使用。
// </summary>
public bool CreateArticle(ArticleService articleService)
{
var dto = new ArticleDto { /* ... 同上 ... */ };
return articleService.AddDataAsync(dto).GetAwaiter().GetResult();
}
流程说明:
AddDataAsync在内部使用UseTranAsync事务 —— 先插入文章主表 → 写入分类关联 → 写入标签关联 → 写入 SEO 数据。任一步骤失败即回滚。
创建文章事务流程:
开始事务
│
├─ 1. INSERT ArtArticle (文章主表)
│
├─ 2. INSERT ArtCategoryArticle[] (分类关联)
│
├─ 3. INSERT ArtArticleTag[] (标签关联)
│
└─ 4. INSERT ArtSeo (SEO 元数据)
│
提交 / 回滚
场景二:查询已发布文章列表(多条件筛选)
// <summary>
// 分页查询已发布文章 —— 按分类/标签/关键词组合筛选。
// </summary>
public async Task<PageResult<ArtArticleOutDto>> SearchArticlesAsync(ArticleService articleService)
{
var param = new ArticleParam
{
PageNo = 1,
PageSize = 10,
Title = "RuoVea", // 标题模糊搜索
Status = 1, // 仅已发布
IsFeatured = YesOrNot.Y, // 仅精选
CategorieIds = new List<long> { 1 },
CategorieIsDisable = YesOrNot.N // 仅启用分类
};
return await articleService.GetPagesPushsAsync(param);
}
场景三:用户点赞/收藏(含去重与计数同步)
// <summary>
// 用户点赞文章 —— 自动去重,并在事务内同步更新文章的 LikeCount。
// </summary>
public async Task<bool> LikeArticleAsync(LikeService likeService, long articleId)
{
var like = new ArtLike
{
ArticleId = articleId
// Creator 由 EntityBase 自动填充当前登录用户 ID
};
return await likeService.AddDataAsync(like);
}
// <summary>
// 检查用户是否已点赞某篇文章
// </summary>
public async Task<bool> CheckIsLikeAsync(LikeService likeService, long userId, long articleId)
{
return await likeService.GetIsLikeAsync(new LikeDto
{
UserId = userId,
ArticleId = articleId
});
}
// <summary>
// 取消点赞 —— 软删除 + LikeCount 回退(⚠️ 线程安全说明见下文)
// </summary>
public async Task CancelLikeAsync(LikeService likeService, long likeId)
{
await likeService.DeleteDataAsync(new EntityBaseId { Id = likeId });
}
点赞事务流程:
IF 已存在(同用户+同文章) → 抛出 D9000(重复操作)
│
开始事务
│
├─ 1. UPDATE ArtArticle SET LikeCount = LikeCount + 1
│
└─ 2. INSERT ArtLike
│
提交
场景四:构建分类树 + 文章列表(前端导航菜单)
// <summary>
// 获取完整分类树(递归构建),附带每个分类下的已发布文章。
// </summary>
public async Task<List<CategoryOutput>> BuildNavMenuAsync(CategoryService categoryService)
{
return await categoryService.GetList(new CategoryQuery
{
IsDisable = YesOrNot.N
});
}
// <summary>
// 获取树形分页结构 —— 按根节点分页,每页返回带完整子节点的树。
// </summary>
public async Task<PageResult<CategoryTreeDto>> GetCategoryTreeAsync(CategoryService categoryService)
{
return await categoryService.GetTreePagesAsync(new CategoryParam
{
PageNo = 1,
PageSize = 20,
PId = 0 // 仅顶级节点
});
}
场景五:楼层式评论
// <summary>
// 发表评论 —— 自动递增文章的 CommentCount。
// </summary>
public async Task<bool> PostCommentAsync(CommentService commentService, CommentDto dto)
{
return await commentService.AddDataAsync(dto);
}
// <summary>
// 获取某篇文章的顶级评论(PId=0),自动 Include 子回复。
// </summary>
public async Task<PageResult<CommentDto>> GetCommentsAsync(
CommentService commentService, long articleId)
{
return await commentService.GetPagesAsync(new CommentParam
{
ArticleId = articleId,
Status = 1, // 仅已发布评论
PageNo = 1,
PageSize = 20
});
}
配置选项详解
数据库连接配置 (ConnectionConfigs)
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./ruovea.db",
// 可选配置
"EnableUnderLine": false,
"EnableDiffLog": false,
"IsEncrypt": false,
"DbSecurity": "",
"IsDeleteFilter": true,
"IsUserIdFilter": false,
"IsTenantIdFilter": false,
"CommandTimeOut": 30
}
]
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DbType |
string | 必填 | 数据库类型 |
ConnectionString |
string | 必填 | 连接字符串 |
EnableUnderLine |
bool | false |
驼峰转下划线 |
EnableDiffLog |
bool | false |
启用库表差异日志 |
IsEncrypt |
bool | false |
连接字符串是否加密 |
DbSecurity |
string | "" |
解密密钥(IsEncrypt=true 时使用) |
IsDeleteFilter |
bool | true |
⚠️ 全局软删除过滤(实体需继承 IDeletedEntity) |
IsUserIdFilter |
bool | false |
按创建者过滤(实体需继承 ICreatorFilter 或 EntityBase) |
IsTenantIdFilter |
bool | false |
按租户过滤(实体需继承 ITenantIdFilter) |
CommandTimeOut |
int | 30 |
SQL 命令超时时间(秒) |
表初始化配置 (DbInitConfig)
{
"DbInitConfig": {
"InitTable": true
}
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
InitTable |
bool | false |
启动时自动检查并创建缺失的数据表(CodeFirst) |
❗ 性能提醒:
AddArticleInitSetup使用Task.Run在后台执行表检查。生产环境首次启动后建议将InitTable设为false,避免每次启动都执行 9 张表的IsAnyTable检查。
DI 注册配置
// <summary>
// AddOmiArticleSetup —— 三种重载,适应不同配置来源。
// </summary>
// 重载 1:自动从全局 AppSettings.Configuration 读取
builder.Services.AddOmiArticleSetup();
// 重载 2:传入自定义 IConfiguration
builder.Services.AddOmiArticleSetup(configuration.GetSection("MyArticle"));
// 重载 3:通过 Action 委托配置
builder.Services.AddOmiArticleSetup(options =>
{
options.InitTable = true;
});
// 自定义服务生命周期
builder.Services.AddOmiArticleSetup(ServiceLifetime.Singleton);
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
serviceLifetime |
ServiceLifetime |
Scoped |
注册 8 个服务的生命周期 |
config |
IConfiguration |
— | 自定义配置节 |
config (Action) |
Action<DbInitConfig> |
— | 代码内配置 |
⚠️ 线程安全: 切换为
Singleton生命周期时,确保注入的SugarRepository<T>和ISqlSugarClient本身支持并发访问。SqlSugar 的SqlSugarClient是线程安全的,但仓储的某些操作依赖请求上下文(如Creator自动填充),单例模式下可能导致用户信息串扰。
API 接口速览
所有接口自动归入 Swagger "art" 分组,默认路由前缀由 DynamicWebApi 配置决定。
ArticleService — 文章管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync |
分页查询(含分类/标签/SEO) |
| GET | GetPagesEnableAsync |
分页查询(仅启用分类) |
| GET | GetPagesPushsAsync |
分页查询(仅已发布 + 启用分类) |
| GET | GetPagesByCategorysAsync |
按分类 ID 查询已发布文章 |
| GET | GetPagesByTagsAsync |
按标签 ID 查询已发布文章 |
| GET | GetList |
轻量列表(Id/Title/Summary/Cover) |
| GET | GetEnableList |
轻量列表(仅启用分类) |
| GET | GetDataAsync |
根据 ID 查详情 |
| GET | GetDataByIdAsync |
根据 long ID 查详情 |
| POST | PlusViewCountAsync |
增加阅读量 |
| POST | AddDataAsync |
创建文章(事务:文章+分类+标签+SEO) |
| POST | UpdateDataAsync |
更新文章(事务:替换关联+SEO Upsert) |
| DELETE | DeleteDataAsync |
删除(软删除/物理删除级联) |
CategoryService — 分类管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync |
分页查询(平坦结构,含子节点) |
| GET | GetTreePagesAsync |
树形分页(先构建完整树,再分页根节点) |
| GET | GetDataAsync |
根据 ID 查详情(含子节点) |
| GET | GetList |
树形列表(含子节点递归) |
| GET | GetListByPage |
平坦 ID/Code/Name 分页列表 |
| POST | AddDataAsync |
创建分类(Name/Code 去重检查) |
| POST | UpdateDataAsync |
更新分类(去重检查排除自身) |
| DELETE | DeleteDataAsync |
删除(有子节点或有文章关联时阻止) |
TagService — 标签管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync |
分页查询(仅未删除) |
| GET | GetAllList |
全部标签列表 |
| GET | GetList |
ID/Code/Name 分页列表 |
| GET | GetListByUserId |
按创建用户筛选 |
| POST | AddDataAsync |
创建标签(Name/Code 去重) |
| POST | UpdateDataAsync |
更新标签 |
| DELETE | DeleteDataAsync |
删除(有关联文章时阻止) |
CommentService / FavoriteService / LikeService
| 服务 | HTTP | 方法 | 说明 |
|---|---|---|---|
| Comment | GET | GetPagesAsync |
分页获取顶级评论(含子回复) |
| Comment | POST | AddDataAsync |
发表评论(事务:+CommentCount) |
| Comment | DELETE | DeleteDataAsync |
删除(软删时 -CommentCount) |
| Favorite | GET | GetPagesAsync |
用户收藏分页 |
| Favorite | GET | GetConntAsync |
文章收藏数 |
| Favorite | GET | GetIsFavoriteAsync |
用户是否已收藏 |
| Favorite | POST | AddDataAsync |
收藏(自动去重) |
| Like | GET | GetPagesAsync |
用户点赞分页 |
| Like | GET | GetConntAsync |
文章点赞数 |
| Like | GET | GetIsLikeAsync |
用户是否已点赞 |
| Like | POST | AddDataAsync |
点赞(去重 + 事务 +LikeCount) |
| Like | DELETE | DeleteDataAsync |
取消赞(软删时 -LikeCount) |
错误处理与日志
错误码速查
组件内部使用 ErrorEnum 和 i18n 国际化资源管理错误信息:
| 错误码 | 含义 | 触发场景 |
|---|---|---|
D1504 |
参数为空 | 传入 DTO 为 null |
D1002 |
数据不存在 | 根据 ID 查询/更新时记录缺失 |
D9000 |
数据已存在 | 创建重复的标签/分类名称或 Code |
D1007 |
存在关联数据 | 删除有子节点或有文章引用的分类/标签 |
invalidintegerformat |
ID 无效 | Id ≤ 0 时触发 |
fieldisrequired |
必填字段为空 | Title/Content/Categories 未提供 |
异常处理示例
// <summary>
// 安全创建文章 —— 捕获参数、业务和数据库异常。
// </summary>
public async Task<(bool Success, string Message)> SafeCreateArticleAsync(
ArticleService articleService, ArticleDto dto)
{
try
{
var result = await articleService.AddDataAsync(dto);
return (true, "文章创建成功");
}
catch (ArgumentException ex)
{
// 业务校验失败(D1504/D1002/D9000)
return (false, $"业务校验失败: {ex.Message}");
}
catch (AggregateException ex)
{
// 字段级验证失败(必填字段缺失、格式错误)
return (false, $"字段验证失败: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("transaction", StringComparison.OrdinalIgnoreCase))
{
// 事务执行失败(数据库层面错误)
return (false, $"数据操作失败,已自动回滚: {ex.Message}");
}
}
// <summary>
// 同步写法 —— 仅在非 ASP.NET 上下文使用
// </summary>
public (bool Success, string Message) SafeCreateArticle(ArticleService articleService, ArticleDto dto)
{
try
{
var result = articleService.AddDataAsync(dto).GetAwaiter().GetResult();
return (true, "文章创建成功");
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
日志集成
组件不直接输出日志,依赖调用方集成的日志框架。推荐在 Program.cs 中配置 Serilog 或 NLog 来捕获:
// SqlSugar 的 SQL 日志可通过 AOP 事件捕获
builder.Services.AddSqlSugarSetup(); // 内部配置了 SQL 执行日志
版本迁移指南
从 8.0.0.x 升级到 10.0.0.x
| 变更项 | 说明 |
|---|---|
| TFM 升级 | net8.0 → net10.0,需同步升级依赖包到 10.0.* 版本 |
| 包版本对齐 | RuoVea.DynamicWebApi 和 RuoVea.ExSugar 需升至对应 10.0.* |
| API 兼容 | 所有公开 API 向后兼容,无需修改业务代码 |
| 数据库 | 表结构无变更,无需执行迁移脚本 |
API 变更历史
| 版本 | 变更 |
|---|---|
8.0.0.19 |
修复多字段查询时 SqlSugar 因重复参数 @value 键报错的问题 |
8.0.0.18 |
组件版本升级,图表数据缓存 |
8.0.0.17 |
表结构初始化处理 |
| 更早版本 | 初始发布 |
常见问题
Q: 如何切换数据库?
修改 appsettings.json 中的 DbType 和 ConnectionString,然后重新运行。AddArticleInitSetup 会自动为新数据库创建表结构。
// MySql 示例
{ "DbType": "MySql", "ConnectionString": "Server=localhost;Database=ruovea;Uid=root;Pwd=123456;" }
// SqlServer 示例
{ "DbType": "SqlServer", "ConnectionString": "Server=.;Database=ruovea;Trusted_Connection=True;" }
// PostgreSQL 示例
{ "DbType": "PostgreSQL", "ConnectionString": "Host=localhost;Database=ruovea;Username=postgres;Password=123456;" }
Q: 如何自定义 API 路由前缀?
在 AddDynamicWebApi 中配置 DefaultApiPrefix:
builder.Services.AddDynamicWebApi(options =>
{
options.DefaultApiPrefix = "/api/v1/article";
});
Q: ⚠️ 软删除后如何恢复?
当前版本软删除操作将 IsDelete 置为 Y,不提供内置恢复接口。如需恢复,请直接操作数据库将对应记录的 IsDelete 字段改为 N。
Q: ❗ 为什么点赞数/评论数可能出现不一致?
点赞和评论操作的计数更新在数据库事务内完成,正常情况保持一致。但在极高并发场景下,如果同时发生取消点赞(LikeCount - 1)操作,LikeCount > 0 的 guard 条件可能导致部分 -1 被跳过。建议定期运行数据校准脚本:
-- 校准文章点赞数
UPDATE ArtArticle SET LikeCount = (
SELECT COUNT(1) FROM ArtLike
WHERE ArtLike.ArticleId = ArtArticle.Id AND ArtLike.IsDelete = 0
);
Q: 分类/标签删除前为何被阻止?
当分类或标签已被文章关联时,为防止数据孤岛,组件会抛出 D1007 错误。解决方案:先解除所有文章关联,再执行删除。
许可证
本项目基于 Apache 2.0 License 开源发布。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- RuoVea.DynamicWebApi (>= 10.0.0)
- RuoVea.ExSugar (>= 10.0.0.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on RuoVea.OmiApi.Article:
| Package | Downloads |
|---|---|
|
RuoVea.OmiArticle
文章分类和内容管理 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.0.6 | 87 | 7/24/2026 |
| 10.0.0.5 | 94 | 7/17/2026 |
| 10.0.0.4 | 91 | 7/15/2026 |
| 10.0.0.3 | 109 | 6/24/2026 |
| 10.0.0.2 | 130 | 5/28/2026 |
| 10.0.0.1 | 150 | 3/23/2026 |
| 9.0.0.3 | 137 | 5/28/2026 |
| 9.0.0.2 | 105 | 5/28/2026 |
| 9.0.0.1 | 145 | 3/23/2026 |
| 9.0.0 | 124 | 1/27/2026 |
| 8.0.0.22 | 92 | 7/24/2026 |
| 8.0.0.21 | 97 | 7/17/2026 |
| 8.0.0.20 | 88 | 7/15/2026 |
| 8.0.0.19 | 117 | 6/24/2026 |
| 8.0.0.18 | 141 | 5/28/2026 |
| 8.0.0.17 | 139 | 5/21/2026 |
| 7.0.0.17 | 140 | 5/28/2026 |
| 7.0.0.16 | 159 | 3/23/2026 |
| 6.0.0.17 | 148 | 5/28/2026 |
| 6.0.0.16 | 168 | 3/23/2026 |