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
<PackageReference Include="RuoVea.OmiApi.Notice" Version="10.0.1.4" />
<PackageVersion Include="RuoVea.OmiApi.Notice" Version="10.0.1.4" />
<PackageReference Include="RuoVea.OmiApi.Notice" />
paket add RuoVea.OmiApi.Notice --version 10.0.1.4
#r "nuget: RuoVea.OmiApi.Notice, 10.0.1.4"
#:package RuoVea.OmiApi.Notice@10.0.1.4
#addin nuget:?package=RuoVea.OmiApi.Notice&version=10.0.1.4
#tool nuget:?package=RuoVea.OmiApi.Notice&version=10.0.1.4
RuoVea.OmiApi.Notice
系统通知公告管理 —— 基于 .NET 构建的通知公告全生命周期管理系统,支持草稿/发布/撤回状态流转与 SignalR 实时广播推送。
RuoVea.OmiApi.Notice 是一个开箱即用的通知公告管理 NuGet 包,提供通知公告的创建、编辑、删除、发布、撤回全生命周期管理能力。支持按用户推送通知并追踪已读/未读状态,通过 SignalR 实现实时广播推送,支持通知(NOTICE)和公告(ANNOUNCEMENT)两种类型,草稿(DRAFT)/ 发布(PUBLIC)/ 撤回(CANCEL)三种状态流转。基于 SqlSugar ORM 和 DynamicWebApi,注册即自动生成 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 值:
MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、OpenGauss、ClickHouse等。
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=sysNotice,Path=/system/notice,Component=@ruovea/notice/notice/index,Icon=ele-Notification,Sort=140,IsKeepAlive=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.0 → net10.0,需同步升级依赖包到 10.0.* 版本 |
| 包版本对齐 | RuoVea.DynamicWebApi、RuoVea.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 中的 DbType 和 ConnectionString,然后重新运行。AddNoticeInitSetup 会自动为新数据库创建 SysNotice 和 SysNoticeUser 两张表。
// 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 广播不到用户怎么办?
检查以下几点:
- 确保
Program.cs中已调用builder.Services.AddSignalR()和app.MapHub<OnlineUserHub>("/hubs/online-user") - 确认前端已连接到 SignalR Hub 并实现了
IOnlineUserHub.PublicNotice客户端方法 - 检查中间件顺序 ——
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] 特性的方法(AddAsync、PublicAsync)在单个数据库事务内执行所有数据操作。AddAsync 仅包含单表 Insert,PublicAsync 包含更新通知状态、删除旧记录、批量插入用户记录三步,任一步失败即整体回滚。SignalR 广播在事务外执行 —— 如果广播失败不影响数据库已提交的数据。
许可证
本项目基于 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.1.3)
- RuoVea.OmiApi.Auth (>= 10.0.1.9)
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 |