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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RuoVea.OmiApi.Article" Version="10.0.0.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RuoVea.OmiApi.Article" Version="10.0.0.6" />
                    
Directory.Packages.props
<PackageReference Include="RuoVea.OmiApi.Article" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RuoVea.OmiApi.Article --version 10.0.0.6
                    
#r "nuget: RuoVea.OmiApi.Article, 10.0.0.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RuoVea.OmiApi.Article@10.0.0.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RuoVea.OmiApi.Article&version=10.0.0.6
                    
Install as a Cake Addin
#tool nuget:?package=RuoVea.OmiApi.Article&version=10.0.0.6
                    
Install as a Cake Tool

RuoVea.OmiApi.Article

文章分类和内容管理 API 组件 —— 基于 .NET 构建的轻量级、跨平台内容管理系统后端。

RuoVea.OmiApi.Article 是一个开箱即用的文章管理系统 NuGet 包,提供文章、分类、标签、评论、点赞、收藏、SEO 元数据的完整 CRUD 能力。基于 SqlSugar ORMDynamicWebApi,注册即自动生成 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 值: MySqlSqlServerSqliteOraclePostgreSQLDmKdbndpOpenGaussClickHouse 等。

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 按创建者过滤(实体需继承 ICreatorFilterEntityBase
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)

错误处理与日志

错误码速查

组件内部使用 ErrorEnumi18n 国际化资源管理错误信息:

错误码 含义 触发场景
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.0net10.0,需同步升级依赖包到 10.0.* 版本
包版本对齐 RuoVea.DynamicWebApiRuoVea.ExSugar 需升至对应 10.0.*
API 兼容 所有公开 API 向后兼容,无需修改业务代码
数据库 表结构无变更,无需执行迁移脚本

API 变更历史

版本 变更
8.0.0.19 修复多字段查询时 SqlSugar 因重复参数 @value 键报错的问题
8.0.0.18 组件版本升级,图表数据缓存
8.0.0.17 表结构初始化处理
更早版本 初始发布

常见问题

Q: 如何切换数据库?

修改 appsettings.json 中的 DbTypeConnectionString,然后重新运行。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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed