RuoVea.OmiApi.Notice 10.0.1.4

dotnet add package RuoVea.OmiApi.Notice --version 10.0.1.4
                    
NuGet\Install-Package RuoVea.OmiApi.Notice -Version 10.0.1.4
                    
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.Notice" Version="10.0.1.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RuoVea.OmiApi.Notice" Version="10.0.1.4" />
                    
Directory.Packages.props
<PackageReference Include="RuoVea.OmiApi.Notice" />
                    
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.Notice --version 10.0.1.4
                    
#r "nuget: RuoVea.OmiApi.Notice, 10.0.1.4"
                    
#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.Notice@10.0.1.4
                    
#: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.Notice&version=10.0.1.4
                    
Install as a Cake Addin
#tool nuget:?package=RuoVea.OmiApi.Notice&version=10.0.1.4
                    
Install as a Cake Tool

RuoVea.OmiApi.Notice

系统通知公告管理 —— 基于 .NET 构建的通知公告全生命周期管理系统,支持草稿/发布/撤回状态流转与 SignalR 实时广播推送。

RuoVea.OmiApi.Notice 是一个开箱即用的通知公告管理 NuGet 包,提供通知公告的创建、编辑、删除、发布、撤回全生命周期管理能力。支持按用户推送通知并追踪已读/未读状态,通过 SignalR 实现实时广播推送,支持通知(NOTICE)和公告(ANNOUNCEMENT)两种类型,草稿(DRAFT)/ 发布(PUBLIC)/ 撤回(CANCEL)三种状态流转。基于 SqlSugar ORMDynamicWebApi,注册即自动生成 RESTful API 端点。


目录


概览

功能特性

模块 功能
📢 通知公告 CRUD 新增、编辑、删除,支持按创建人隔离查询和按标题/类型分页筛选
🔄 状态流转 草稿 (DRAFT) → 发布 (PUBLIC) → 撤回 (CANCEL) 三状态流转控制
📋 双类型支持 通知 (NOTICE=1) 和公告 (ANNOUNCEMENT=2) 两种业务类型
👤 用户已读追踪 基于 SysNoticeUser 表的用户级已读/未读状态追踪
📡 实时广播 SignalR (OnlineUserHub) 实时推送通知给所有在线用户
📦 批量分发 发布时自动为所有有效用户创建通知记录并清理旧记录
🌐 多语言 i18n 通过 .resx 资源文件支持 zh-CN、en-US、ja-JP、fr-FR、vi-VN、zh-HK、zh-TW
🗄️ CodeFirst 建表 自动创建 SysNotice 和 SysNoticeUser 两张数据表
🧩 自动 API 实现 IApplicationService 即自动映射为 REST 控制器

架构一览

┌─────────────────────────────────────────────────────┐
│                   NuGet Package                      │
│  RuoVea.OmiApi.Notice                               │
├─────────────────────────────────────────────────────┤
│  Service Layer                                       │
│  ┌─────────────────────────────────────────┐        │
│  │          SysNoticeService               │        │
│  │  · GetPagesAsync (管理员分页查询)        │        │
│  │  · GetListAsync (前端已发布列表)         │        │
│  │  · AddAsync / UpdateAsync / DeleteAsync │        │
│  │  · PublicAsync (发布+广播)              │        │
│  │  · CancelAsync (撤回)                   │        │
│  │  · GetUnReadListAsync (未读列表)        │        │
│  │  · PageReceivedAsync (通知中心)         │        │
│  │  · SetReadAsync (标记已读)              │        │
│  └─────────────────────────────────────────┘        │
├─────────────────────────────────────────────────────┤
│  Domain Layer (2 Entities)                          │
│  SysNotice ──< SysNoticeUser (1:N, NoticeId FK)     │
│  SysNotice: NoticeTypeEnum / NoticeStatusEnum        │
│  SysNoticeUser: NoticeReadStatusEnum                 │
├─────────────────────────────────────────────────────┤
│  Infrastructure                                      │
│  SqlSugar ORM  ·  SignalR Hub  ·  DynamicWebApi      │
│  OnlineUserHub (IOnlineUserHub.PublicNotice)          │
│  SugarRepository<T> (ExSugar 仓储模式)               │
│  ICurrentUser (当前用户上下文)                        │
└─────────────────────────────────────────────────────┘

状态流转图

                    ┌──────────┐
                    │  DRAFT   │  (草稿)
                    │    0     │
                    └────┬─────┘
                         │ 发布 (PublicAsync)
                         ▼
                    ┌──────────┐
          ┌────────>│  PUBLIC  │  (已发布)
          │         │    1     │
          │         └────┬─────┘
          │              │ 撤回 (CancelAsync)
          │              ▼
          │         ┌──────────┐
          └─────────│  CANCEL  │  (已撤回)
        再次发布     │    2     │
                    └──────────┘

支持的 .NET 版本

TFM NuGet 版本
net8.0 8.0.1.3
net10.0 10.0.1.3

安装

NuGet 包管理器

# .NET 8 项目
Install-Package RuoVea.OmiApi.Notice -Version 8.0.1.3

# .NET 10 项目
Install-Package RuoVea.OmiApi.Notice -Version 10.0.1.3

.NET CLI

dotnet add package RuoVea.OmiApi.Notice --version 8.0.1.3

依赖项

本包依赖以下组件(安装时会自动引入):

包名 用途
RuoVea.DynamicWebApi 动态 API 控制器生成
RuoVea.ExSugar SqlSugar 仓储模式封装、EntityBase 基类
RuoVea.OmiApi.Auth 认证模块(提供 SysUser 实体、ICurrentUser),固定引用 8.0.2.10(net8.0)/ 10.0.1.8(net10.0)
Microsoft.AspNetCore.App ASP.NET Core 框架引用(仅 net8.0),提供 SignalR 支持

30 秒快速开始

1. 配置数据库连接 (appsettings.json)

{
  "ConnectionConfigs": [
    {
      "DbType": "Sqlite",
      "ConnectionString": "DataSource=./ruovea.db"
    }
  ],
  "DbInitConfig": {
    "InitTable": true
  },
  "Swagger": {
    "ApiVersions": [
      {
        "Title": "系统管理",
        "Version": "system"
      }
    ]
  }
}

支持的 DbType 值: MySqlSqlServerSqliteOraclePostgreSQLDmKdbndpOpenGaussClickHouse 等。

2. 注册服务 (Program.cs)

// <summary>
// 在 Program.cs 中注册 OmiApi.Notice 组件服务
// </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" };
});

// 注册 Notice 模块服务(默认 Scoped 生命周期)
builder.Services.AddOmiNoticeSetup();

// 注册 SqlSugar ORM
builder.Services.AddSqlSugarSetup();

// 注册 SignalR(实时推送需要)
builder.Services.AddSignalR();

// 初始化数据库表结构(异步建表)
builder.Services.AddNoticeInitSetup();

var app = builder.Build();

// 映射 SignalR Hub
app.MapHub<OnlineUserHub>("/hubs/online-user");

app.Run();

3. 启动并访问 Swagger

启动项目后,访问 https://localhost:xxxx/swagger,即可看到 "system" 分组下的全部通知公告 RESTful API 端点。


核心场景

场景一:创建并发布一条通知公告(含实时广播)

// <summary>
// 创建通知公告并发布 —— 异步写法。发布时自动为所有有效用户创建已读记录,
// 并通过 SignalR 向所有在线用户实时广播。
// </summary>
public async Task PublishNoticeAsync(SysNoticeService noticeService)
{
    // 1. 创建通知公告(自动设为草稿状态)
    var input = new SysNoticeInputDto
    {
        Title   = "系统维护通知",
        Content = "系统将于今晚 22:00-24:00 进行维护升级,届时请勿登录系统。",
        Type    = 1,     // 1=通知 (NOTICE)
        Sort    = 100
    };

    var created = await noticeService.AddAsync(input);

    // 2. 发布通知公告(触发 SignalR 广播)
    if (created)
    {
        // 查找刚创建的通知 ID
        var list = await noticeService.GetPagesAsync(new SysNoticeParam
        {
            Title    = "系统维护通知",
            PageNo   = 1,
            PageSize = 1
        });

        if (list.Items.Any())
        {
            await noticeService.PublicAsync(new EntityBaseIdDto { Id = list.Items.First().Id });
        }
    }
}

// <summary>
// 同步写法 —— 仅在非 ASP.NET 上下文使用
// ⚠️ 注意:在 ASP.NET 上下文中可能导致死锁,仅推荐在 Console/测试环境使用。
// </summary>
public void PublishNotice(SysNoticeService noticeService)
{
    var input = new SysNoticeInputDto
    {
        Title   = "系统维护通知",
        Content = "系统将于今晚 22:00-24:00 进行维护升级。",
        Type    = 1
    };

    noticeService.AddAsync(input).GetAwaiter().GetResult();

    var list = noticeService.GetPagesAsync(new SysNoticeParam
    {
        Title    = "系统维护通知",
        PageNo   = 1,
        PageSize = 1
    }).GetAwaiter().GetResult();

    if (list.Items.Any())
    {
        noticeService.PublicAsync(new EntityBaseIdDto { Id = list.Items.First().Id })
            .GetAwaiter().GetResult();
    }
}

流程说明: PublicAsync 内部执行以下操作序列:验证通知存在且未发布 → 更新状态为 PUBLIC、记录发布时间/发布人 → 查询所有有效用户 → 删除旧的用户通知记录 → 批量插入所有用户的未读记录 → 通过 SignalR _hubContext.Clients.All.PublicNotice(notice) 实时广播给全部在线用户。

发布通知事务流程 (PublicAsync):

  开始事务 (UnitOfWork)
    │
    ├─ 1. 验证通知存在且属于当前用户
    │
    ├─ 2. 检查状态 != PUBLIC(已发布不可重复发布)
    │
    ├─ 3. UPDATE SysNotice SET Status=1, PublicTime=Now, PublicUserName=...
    │
    ├─ 4. SELECT Id FROM SysUser WHERE IsDelete=0  (获取所有有效用户)
    │
    ├─ 5. DELETE FROM SysNoticeUser WHERE NoticeId = @noticeId  (清理旧记录)
    │
    ├─ 6. BULK INSERT SysNoticeUser (NoticeId, UserId, ReadStatus=UNREAD)
    │
    └─ 7. SignalR: _hubContext.Clients.All.PublicNotice(notice)
    │
  提交事务 / 异常回滚

场景二:用户获取未读通知列表并标记已读

// <summary>
// 获取当前用户的所有未读通知列表 —— 异步写法。
// </summary>
public async Task<List<SysNoticeDto>> GetMyUnreadNoticesAsync(SysNoticeService noticeService)
{
    // 返回当前用户所有未读的已发布通知,按发布时间倒序
    var unreadList = await noticeService.GetUnReadListAsync();

    foreach (var notice in unreadList)
    {
        Console.WriteLine($"[未读] {notice.Title} - 发布于 {notice.PublicTime}");
    }

    return unreadList;
}

// <summary>
// 标记某条通知为已读 —— 边缘情况自动创建记录。
// </summary>
public async Task MarkAsReadAsync(SysNoticeService noticeService, long noticeId)
{
    // 如果 SysNoticeUser 已有记录则更新 ReadStatus=READ
    // 如果不存在(边缘情况:发布后新增的用户),则创建记录并标记已读
    await noticeService.SetReadAsync(new EntityBaseIdDto { Id = noticeId });
}

// <summary>
// 同步写法
// </summary>
public void GetMyUnreadNotices(SysNoticeService noticeService)
{
    var unreadList = noticeService.GetUnReadListAsync().GetAwaiter().GetResult();

    foreach (var notice in unreadList)
    {
        Console.WriteLine($"[未读] {notice.Title} - 发布于 {notice.PublicTime}");
        noticeService.SetReadAsync(new EntityBaseIdDto { Id = notice.Id })
            .GetAwaiter().GetResult();
    }
}
用户标记已读流程 (SetReadAsync):

  查找 SysNoticeUser WHERE NoticeId=@id AND UserId=@currentUser
    │
    ├─ 记录存在 → UPDATE SET ReadStatus=READ, ReadTime=Now
    │
    └─ 记录不存在(边缘情况)
         → INSERT SysNoticeUser (NoticeId, UserId, ReadStatus=READ, ReadTime=Now)

场景三:通知公告撤回与再次发布

// <summary>
// 撤回已发布的通知公告 —— 仅 PUBLISHED 状态可撤回。
// 撤回后状态变为 CANCEL,数据保留,可再次发布。
// </summary>
public async Task CancelAndRepublishAsync(SysNoticeService noticeService, long noticeId)
{
    // 撤回通知
    await noticeService.CancelAsync(new EntityBaseIdDto { Id = noticeId });
    // 状态: PUBLIC(1) -> CANCEL(2)

    // 业务需要时,可以再次发布
    // 注意:从 CANCEL 状态可以再次调用 PublicAsync 重新发布
    await noticeService.PublicAsync(new EntityBaseIdDto { Id = noticeId });
    // 状态: CANCEL(2) -> PUBLIC(1),会重新创建用户未读记录并广播
}

// <summary>
// 同步写法
// </summary>
public void CancelAndRepublish(SysNoticeService noticeService, long noticeId)
{
    noticeService.CancelAsync(new EntityBaseIdDto { Id = noticeId }).GetAwaiter().GetResult();
    noticeService.PublicAsync(new EntityBaseIdDto { Id = noticeId }).GetAwaiter().GetResult();
}
撤回流程 (CancelAsync):

  验证:
    ├─ Id == 0? → 抛出 notice_cancel_fail
    ├─ 不存在或不属于当前用户? → 抛出 notice_not_exist
    └─ Status != PUBLIC? → 抛出 notice_cancel_only_public

  执行:
    └─ UPDATE SysNotice SET Status = CANCEL(2)

场景四:用户通知中心分页查询

// <summary>
// 用户通知中心 —— 获取当前用户接收到的所有已发布通知,含已读/未读状态。
// 适用于前端"消息中心"或"通知铃铛"页面。
// </summary>
public async Task<PageResult<SysNoticeUserDto>> GetMyNoticeCenterAsync(SysNoticeService noticeService)
{
    // 分页查询当前用户收到的通知公告,含读取状态
    var result = await noticeService.PageReceivedAsync(new SysNoticeParam
    {
        PageNo   = 1,
        PageSize = 20,
        Title    = null,   // 可选:按标题模糊搜索
        Type     = null    // 可选:按类型筛选(1=通知 2=公告)
    });

    foreach (var item in result.Items)
    {
        var readLabel = item.ReadStatus == 0 ? "未读" : "已读";
        Console.WriteLine($"[{readLabel}] {item.Title} - 发布人: {item.PublicUserName}");
    }

    return result;
}

// <summary>
// 同步写法
// </summary>
public PageResult<SysNoticeUserDto> GetMyNoticeCenter(SysNoticeService noticeService)
{
    return noticeService.PageReceivedAsync(new SysNoticeParam
    {
        PageNo   = 1,
        PageSize = 20
    }).GetAwaiter().GetResult();
}
通知中心查询 (PageReceivedAsync):

  SELECT  FROM SysNoticeUser nu
  INNER JOIN SysNotice n ON nu.NoticeId = n.Id
  WHERE nu.UserId = @currentUserId
    AND n.Status = PUBLIC(1)
    AND (可选) n.Title LIKE @title
    AND (可选) n.Type = @type
  ORDER BY n.PublicTime DESC
  LIMIT @pageSize OFFSET @offset

场景五:管理员查询通知公告列表

// <summary>
// 管理员分页查询自己创建的通知公告(含草稿/已发布/已撤回全部状态)。
// 查询按创建人隔离,仅返回当前用户创建的通知。
// </summary>
public async Task<PageResult<SysNoticeDto>> AdminQueryNoticesAsync(SysNoticeService noticeService)
{
    // 管理员端分页查询(所有状态)
    var result = await noticeService.GetPagesAsync(new SysNoticeParam
    {
        PageNo   = 1,
        PageSize = 10,
        Title    = "维护",   // 可选:标题模糊搜索
        Type     = 1         // 可选:类型筛选(1=通知 2=公告),null=全部
    });

    foreach (var item in result.Items)
    {
        var statusLabel = item.Status switch
        {
            0 => "草稿",
            1 => "已发布",
            2 => "已撤回",
            _ => "未知"
        };
        Console.WriteLine($"[{statusLabel}] {item.Title}");
    }

    return result;
}

// <summary>
// 前端展示用 —— 仅返回已发布的通知公告。
// </summary>
public async Task<PageResult<SysNoticeDto>> GetPublicNoticesAsync(SysNoticeService noticeService)
{
    // 前端分页查询(仅已发布状态),默认每页 100 条
    return await noticeService.GetListAsync(new SysNoticeParam
    {
        PageNo   = 1,
        PageSize = 100,
        Title    = null
    });
}

配置选项详解

数据库连接配置 (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 全局软删除过滤
IsUserIdFilter bool false 按创建者过滤(实体需继承 EntityBase
IsTenantIdFilter bool false 按租户过滤
CommandTimeOut int 30 SQL 命令超时时间(秒)

表初始化配置 (DbInitConfig)

{
  "DbInitConfig": {
    "InitTable": true
  }
}
参数 类型 默认值 说明
InitTable bool false 启动时是否执行 CodeFirst.InitTables 自动建表并同步表结构与新增列

性能提醒: AddNoticeInitSetup 使用 Task.Run 在后台执行表检查。生产环境首次启动后建议将 InitTable 设为 false,避免每次启动都执行 IsTableExists 检查。

DI 注册配置

// <summary>
// AddOmiNoticeSetup —— 三种重载,适应不同配置来源。
// </summary>

// 重载 1:自动从全局 AppSettings.Configuration 读取
builder.Services.AddOmiNoticeSetup();

// 重载 2:传入自定义 IConfiguration
builder.Services.AddOmiNoticeSetup(configuration.GetSection("MyNotice"));

// 重载 3:通过 Action 委托配置
builder.Services.AddOmiNoticeSetup(options =>
{
    options.InitTable = true;
});

// 自定义服务生命周期
builder.Services.AddOmiNoticeSetup(ServiceLifetime.Singleton);
参数 类型 默认值 说明
serviceLifetime ServiceLifetime Scoped SysNoticeService 的 DI 生命周期
config IConfiguration 自定义配置节
config (Action) Action<DbInitConfig> 代码内配置 DbInitConfig

⚠️ 线程安全: 切换为 Singleton 生命周期时,注意 SysNoticeService 依赖 ICurrentUser(请求作用域),单例模式下可能导致用户信息串扰。推荐保持默认 Scoped

AddNoticeInitSetup 配置

// <summary>
// AddNoticeInitSetup —— 初始化数据库表结构。
// 异步执行 CodeFirst 建表,依赖 DbInitConfig.InitTable 配置。
// </summary>
builder.Services.AddNoticeInitSetup();

// 该扩展内部逻辑:
// 1. 读取 DbInitConfig.InitTable
// 2. 若 InitTable=true,始终执行 CodeFirst.InitTables<SysNotice>()
// 3. 始终执行 CodeFirst.InitTables<SysNoticeUser>()(表已存在则自动对齐结构,保证新增列同步)
// 4. 使用 Task.Run 在后台线程执行,不阻塞启动

菜单种子数据(MenuData)

组件内置 MenuData 静态菜单种子数据,由宿主应用反射映射后插入 SysMenu 表,用于前端菜单与按钮权限初始化。菜单挂载在父级菜单 1310000000101(系统管理)下,noticePId = 1311000000381

类型 标题 说明
菜单 (MENU) 通知公告 Name=sysNoticePath=/system/noticeComponent=@ruovea/notice/notice/indexIcon=ele-NotificationSort=140IsKeepAlive=true
按钮 (BTN) 查询 权限标识 sysNotice:page
按钮 (BTN) 新增 权限标识 sysNotice:add
按钮 (BTN) 修改 权限标识 sysNotice:update
按钮 (BTN) 删除 权限标识 sysNotice:delete
按钮 (BTN) 发布 权限标识 sysNotice:public
按钮 (BTN) 撤回 权限标识 sysNotice:cancel

注:AddNoticeInitSetup 仅负责建表(SysNotice / SysNoticeUser),不直接插入业务种子数据;菜单种子通过宿主「菜单种子扫描」机制读取 MenuData 完成初始化。


API 接口速览

所有接口自动归入 Swagger "system" 分组,默认路由前缀由 DynamicWebApi 配置决定,接口均需登录认证(JWT)。管理端接口(GetPagesAsync / AddAsync / UpdateAsync / DeleteAsync / PublicAsync / CancelAsync)按创建人隔离,仅能操作 Creator == 当前用户 的数据;接收端接口(GetListAsync / GetUnReadListAsync / PageReceivedAsync / SetReadAsync)按当前登录用户身份处理。

SysNoticeService — 通知公告管理

HTTP 方法 方法签名 说明
GET GetPagesAsync GetPagesAsync([FromQuery] SysNoticeParam data) 管理员分页查询(仅当前用户创建,所有状态),按标题/类型筛选
GET GetListAsync GetListAsync([FromQuery] SysNoticeParam data) 前端已发布通知公告分页列表(默认 pageSize=100)
POST AddAsync AddAsync(SysNoticeInputDto data) 新增通知公告(状态自动设为 DRAFT,[UnitOfWork] 事务保护)
PUT UpdateAsync UpdateAsync(SysNoticeInputDto data) 修改通知公告(已发布不可编辑,需先撤回,修改后状态回置 DRAFT)
DELETE DeleteAsync DeleteAsync([FromQuery] EntityBaseIdDto data) 删除通知公告及关联的用户通知记录
POST PublicAsync PublicAsync(EntityBaseIdDto data) 发布通知公告:状态改为 PUBLIC、记录时间/发布人、为所有有效用户创建通知记录、SignalR 实时广播
POST CancelAsync CancelAsync(EntityBaseIdDto data) 撤回通知公告(仅已发布状态可撤回)
GET GetUnReadListAsync GetUnReadListAsync() 获取当前用户的未读通知列表
GET PageReceivedAsync PageReceivedAsync([FromQuery] SysNoticeParam data) 通知中心分页查询(当前用户已接收的通知,含读取状态)
POST SetReadAsync SetReadAsync(EntityBaseIdDto data) 标记通知为已读(边缘情况自动创建用户记录)

错误处理与日志

错误码速查

组件内部使用 i18n 国际化资源管理错误信息,所有错误以 ArgumentException 抛出:

错误码 (i18n key) 含义 触发场景
notice_add_fail 添加通知公告失败 AddAsync 中 Insert 返回 false
notice_not_exist 通知公告不存在 根据 ID 查询/更新/发布/撤回时记录不存在或不属于当前用户
notice_published_cannot_edit 已发布的通知不能编辑,请先撤回 对 Status=PUBLIC 的记录调用 UpdateAsync
notice_delete_fail 删除通知公告失败 DeleteAsync 中 Id=0
notice_publish_fail 发布失败 PublicAsync 中 Id=0
notice_already_published 该通知已发布 重复对 Status=PUBLIC 的记录调用 PublicAsync
notice_cancel_fail 撤回失败 CancelAsync 中 Id=0
notice_cancel_only_public 只能撤回已发布的通知 对 Status!=PUBLIC 的记录调用 CancelAsync

异常处理示例

// <summary>
// 安全操作通知 —— 异步写法。捕获参数、业务和数据库异常。
// </summary>
public async Task<(bool Success, string Message)> SafePublishNoticeAsync(
    SysNoticeService noticeService, long noticeId)
{
    try
    {
        await noticeService.PublicAsync(new EntityBaseIdDto { Id = noticeId });
        return (true, "通知发布成功");
    }
    catch (ArgumentException ex)
    {
        // 业务校验失败(notice_not_exist / notice_already_published / notice_publish_fail)
        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) SafePublishNotice(SysNoticeService noticeService, long noticeId)
{
    try
    {
        noticeService.PublicAsync(new EntityBaseIdDto { Id = noticeId }).GetAwaiter().GetResult();
        return (true, "通知发布成功");
    }
    catch (ArgumentException ex)
    {
        return (false, $"业务校验失败: {ex.Message}");
    }
    catch (Exception ex)
    {
        return (false, ex.Message);
    }
}

日志集成

组件不直接输出日志,依赖调用方集成的日志框架。推荐在 Program.cs 中配置 Serilog 或 NLog 来捕获:

// SqlSugar 的 SQL 日志可通过 AOP 事件捕获
builder.Services.AddSqlSugarSetup(); // 内部配置了 SQL 执行日志

版本迁移指南

从 8.0 升级至 10.0

变更项 说明
TFM 升级 net8.0net10.0,需同步升级依赖包到 10.0.* 版本
包版本对齐 RuoVea.DynamicWebApiRuoVea.ExSugar 需升至对应 10.0.*RuoVea.OmiApi.Auth 固定为 10.0.1.8
API 兼容 所有公开 API 向后兼容,无需修改业务代码
数据库 表结构无变更,无需执行迁移脚本
FrameworkReference net8.0 使用 Microsoft.AspNetCore.App FrameworkReference,net10.0 版本已隐式包含

API 变更历史

版本 变更
8.0.1.3 / 10.0.1.3 统一 InitTable 建表逻辑(始终执行 CodeFirst.InitTables 同步表结构与新增列)、菜单种子数据 MenuData 重构为 record 具名参数形式(含 sysNotice 菜单及查询/新增/修改/删除/发布/撤回按钮权限)、依赖组件版本升级
8.0.0.1 / 10.0.0.1 初始发布:通知公告全生命周期管理、SignalR 实时推送、双类型三状态流转、用户已读追踪

常见问题

Q: 如何切换数据库?

修改 appsettings.json 中的 DbTypeConnectionString,然后重新运行。AddNoticeInitSetup 会自动为新数据库创建 SysNoticeSysNoticeUser 两张表。

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

Q: ⚠️ 为什么发布通知时需要 SysUser 表?

PublicAsync 方法会查询 SysUser 表中所有有效用户(IsDelete=0),以便为每个用户创建一条 SysNoticeUser 未读记录。这要求项目中已安装 RuoVea.OmiApi.Auth 包且 SysUser 表已初始化。

Q: ⚠️ 已发布的通知为什么不能直接编辑?

设计上已发布的通知处于只读状态,防止用户看到的内容被悄悄修改。如需修改,应遵循以下流程:

PUBLIC -> CancelAsync (撤回) -> UpdateAsync (修改) -> PublicAsync (重新发布)

撤回后通知状态变为 CANCEL,用户端的已读记录会保留。重新发布时会为所有有效用户重新创建未读记录。

Q: ❗ 发布通知时 SignalR 广播不到用户怎么办?

检查以下几点:

  1. 确保 Program.cs 中已调用 builder.Services.AddSignalR()app.MapHub<OnlineUserHub>("/hubs/online-user")
  2. 确认前端已连接到 SignalR Hub 并实现了 IOnlineUserHub.PublicNotice 客户端方法
  3. 检查中间件顺序 —— app.UseRouting()app.UseEndpoints() 需在 MapHub 之前
// 前端 SignalR 连接示例
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/online-user")
    .build();

connection.on("PublicNotice", function(notice) {
    console.log("收到新通知:", notice.title);
    // 刷新通知铃铛、显示弹窗等
});

connection.start();

Q: SetReadAsync 为什么有"边缘情况自动创建记录"逻辑?

当通知发布后,如果有新用户注册(SysUser 新增),此时后来注册的用户不在之前的 SysNoticeUser 批量插入范围中。该用户查看通知时调用 SetReadAsync,由于 SysNoticeUser 表中没有他的记录,组件会自动创建一条已读记录,防止用户看到不存在的未读通知引发空异常。

Q: ❗ 发布/修改操作的事务范围是什么?

带有 [UnitOfWork] 特性的方法(AddAsyncPublicAsync)在单个数据库事务内执行所有数据操作。AddAsync 仅包含单表 Insert,PublicAsync 包含更新通知状态、删除旧记录、批量插入用户记录三步,任一步失败即整体回滚。SignalR 广播在事务外执行 —— 如果广播失败不影响数据库已提交的数据。


许可证

本项目基于 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

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
10.0.1.4 97 8/20/2026
10.0.1.3 96 8/16/2026
10.0.1.2 93 8/15/2026
10.0.1.1 97 8/15/2026
10.0.1 95 8/15/2026
10.0.0.4 103 7/24/2026
10.0.0.3 103 7/17/2026
10.0.0.2 110 7/15/2026
10.0.0.1 117 6/24/2026
10.0.0 108 6/5/2026
8.0.1.4 98 8/20/2026
8.0.1.3 102 8/16/2026
8.0.1.2 98 8/15/2026
8.0.1.1 95 8/15/2026
8.0.1 99 8/15/2026
8.0.0.4 138 7/24/2026
8.0.0.3 102 7/17/2026
8.0.0.2 104 7/15/2026
8.0.0.1 130 6/24/2026
8.0.0 134 6/5/2026