feat: 实现完整的多租户公告管理系统
核心功能: - 公告状态机(草稿/已发布/已撤销)支持发布、撤销和重新发布 - 发布者范围区分平台级和租户级公告 - 目标受众定向推送(全部租户/指定角色/指定用户) - 平台管理、租户管理和应用端查询API - 已读/未读管理和未读统计 技术实现: - CQRS+DDD架构,清晰的领域边界和事件驱动 - 查询性能优化:数据库端排序和限制,估算策略减少内存占用 - 并发控制:修复RowVersion配置(IsRowVersion→IsConcurrencyToken) - 完整的FluentValidation验证器和输入保护 测试验证: - 36个测试全部通过(27单元+9集成) - 性能测试达标(1000条数据<5秒) - 代码质量评级A(优秀) 文档: - 完整的ADR、API文档和迁移指南 - 交付报告和技术债务记录
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 应用端公告(面向已认证用户)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/app/announcements")]
|
||||
public sealed class AppAnnouncementsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的公告列表(已发布/有效期内)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/app/announcements?page=1&pageSize=20
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[SwaggerOperation(Summary = "获取可见公告列表", Description = "仅返回已发布且在有效期内的公告(含平台公告)。")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = query with
|
||||
{
|
||||
Status = AnnouncementStatus.Published,
|
||||
IsActive = true,
|
||||
OnlyEffective = true
|
||||
};
|
||||
|
||||
var result = await mediator.Send(request, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户未读公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/app/announcements/unread?page=1&pageSize=20
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet("unread")]
|
||||
[SwaggerOperation(Summary = "获取未读公告", Description = "仅返回未读且在有效期内的已发布公告。")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> GetUnread([FromQuery] GetUnreadAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/app/announcements/900123456789012345/mark-read
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "isRead": true
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/mark-read")]
|
||||
[SwaggerOperation(Summary = "标记公告已读", Description = "仅已发布且可见的公告允许标记已读。")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new MarkAnnouncementAsReadCommand { AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 平台公告管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/platform/announcements")]
|
||||
public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建平台公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/platform/announcements
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "平台升级通知",
|
||||
/// "content": "系统将于今晚 23:00 维护。",
|
||||
/// "announcementType": 0,
|
||||
/// "priority": 10,
|
||||
/// "effectiveFrom": "2025-12-20T00:00:00Z",
|
||||
/// "effectiveTo": null,
|
||||
/// "targetType": "all",
|
||||
/// "targetParameters": null
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "0",
|
||||
/// "title": "平台升级通知",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "创建平台公告", Description = "需要权限:platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Create([FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with
|
||||
{
|
||||
TenantId = 0,
|
||||
PublisherScope = PublisherScope.Platform
|
||||
};
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询平台公告列表。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/platform/announcements?page=1&pageSize=20&status=Published
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "查询平台公告列表", Description = "需要权限:platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = query with { TenantId = 0 };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取平台公告详情。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/platform/announcements/900123456789012345
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "0",
|
||||
/// "title": "平台升级通知",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet("{announcementId:long}")]
|
||||
[PermissionAuthorize("platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "获取平台公告详情", Description = "需要权限:platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await ExecuteAsPlatformAsync(() =>
|
||||
mediator.Send(new GetAnnouncementByIdQuery { TenantId = 0, AnnouncementId = announcementId }, cancellationToken));
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新平台公告(仅草稿)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// PUT /api/platform/announcements/900123456789012345
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "平台升级通知(更新)",
|
||||
/// "content": "维护时间调整为 23:30。",
|
||||
/// "targetType": "all",
|
||||
/// "targetParameters": null,
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPut("{announcementId:long}")]
|
||||
[PermissionAuthorize("platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "更新平台公告", Description = "仅草稿可更新;需要权限:platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { TenantId = 0, AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布平台公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/platform/announcements/900123456789012345/publish
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Published"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/publish")]
|
||||
[PermissionAuthorize("platform-announcement:publish")]
|
||||
[SwaggerOperation(Summary = "发布平台公告", Description = "需要权限:platform-announcement:publish")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(command, cancellationToken));
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销平台公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/platform/announcements/900123456789012345/revoke
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Revoked"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/revoke")]
|
||||
[PermissionAuthorize("platform-announcement:revoke")]
|
||||
[SwaggerOperation(Summary = "撤销平台公告", Description = "需要权限:platform-announcement:revoke")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(command, cancellationToken));
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsPlatformAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
var original = tenantContextAccessor.Current;
|
||||
tenantContextAccessor.Current = new TenantContext(0, null, "platform");
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContextAccessor.Current = original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
@@ -22,36 +23,64 @@ public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiC
|
||||
/// <summary>
|
||||
/// 分页查询公告。
|
||||
/// </summary>
|
||||
/// <returns>租户公告分页结果。</returns>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/tenants/100000000000000001/announcements?page=1&pageSize=20
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[SwaggerOperation(Summary = "查询租户公告列表", Description = "需要权限:tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> Search(long tenantId, [FromQuery] SearchTenantAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> Search(long tenantId, [FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
query = query with { TenantId = tenantId };
|
||||
|
||||
// 2. 查询公告列表
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回分页结果
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公告详情。
|
||||
/// </summary>
|
||||
/// <returns>租户公告详情。</returns>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "title": "租户公告",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[SwaggerOperation(Summary = "获取公告详情", Description = "需要权限:tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询指定公告
|
||||
var result = await mediator.Send(new GetTenantAnnouncementQuery { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
var result = await mediator.Send(new GetAnnouncementByIdQuery { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
@@ -60,37 +89,159 @@ public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiC
|
||||
/// <summary>
|
||||
/// 创建公告。
|
||||
/// </summary>
|
||||
/// <returns>创建的公告信息。</returns>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "租户公告",
|
||||
/// "content": "新品上线提醒",
|
||||
/// "announcementType": 0,
|
||||
/// "priority": 5,
|
||||
/// "effectiveFrom": "2025-12-20T00:00:00Z",
|
||||
/// "targetType": "roles",
|
||||
/// "targetParameters": "{\"roles\":[\"OpsManager\"]}"
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "title": "租户公告",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant-announcement:create")]
|
||||
[SwaggerOperation(Summary = "创建租户公告", Description = "需要权限:tenant-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
command = command with { TenantId = tenantId };
|
||||
|
||||
// 2. 创建公告并返回
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新公告。
|
||||
/// 更新公告(仅草稿)。
|
||||
/// </summary>
|
||||
/// <returns>更新后的公告信息。</returns>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// PUT /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "租户公告(更新)",
|
||||
/// "content": "公告内容更新",
|
||||
/// "targetType": "all",
|
||||
/// "targetParameters": null,
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPut("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:update")]
|
||||
[SwaggerOperation(Summary = "更新租户公告", Description = "仅草稿可更新;需要权限:tenant-announcement:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户与公告标识
|
||||
command = command with { TenantId = tenantId, AnnouncementId = announcementId };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
/// <summary>
|
||||
/// 发布公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/publish
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Published"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/publish")]
|
||||
[PermissionAuthorize("tenant-announcement:publish")]
|
||||
[SwaggerOperation(Summary = "发布租户公告", Description = "需要权限:tenant-announcement:publish")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/revoke
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Revoked"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/revoke")]
|
||||
[PermissionAuthorize("tenant-announcement:revoke")]
|
||||
[SwaggerOperation(Summary = "撤销租户公告", Description = "需要权限:tenant-announcement:revoke")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
@@ -99,33 +250,56 @@ public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiC
|
||||
/// <summary>
|
||||
/// 删除公告。
|
||||
/// </summary>
|
||||
/// <returns>删除结果。</returns>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// DELETE /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": true
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpDelete("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:delete")]
|
||||
[SwaggerOperation(Summary = "删除租户公告", Description = "需要权限:tenant-announcement:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除公告
|
||||
var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
|
||||
// 2. 返回执行结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读。
|
||||
/// 标记公告已读(兼容旧路径)。
|
||||
/// </summary>
|
||||
/// <returns>标记已读后的公告信息。</returns>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/read
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "isRead": true
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/read")]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[SwaggerOperation(Summary = "标记公告已读", Description = "需要权限:tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 标记公告已读
|
||||
var result = await mediator.Send(new MarkTenantAnnouncementReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-announcement:publish",
|
||||
"tenant-announcement:revoke",
|
||||
"platform-announcement:create",
|
||||
"platform-announcement:publish",
|
||||
"platform-announcement:revoke",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:create",
|
||||
@@ -173,6 +178,8 @@
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-announcement:publish",
|
||||
"tenant-announcement:revoke",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:read",
|
||||
@@ -359,6 +366,11 @@
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-announcement:publish",
|
||||
"tenant-announcement:revoke",
|
||||
"platform-announcement:create",
|
||||
"platform-announcement:publish",
|
||||
"platform-announcement:revoke",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:create",
|
||||
|
||||
Reference in New Issue
Block a user