feat: 新增公告管理

This commit is contained in:
ccnetcore
2026-01-24 22:08:54 +08:00
parent 1ada6360d4
commit 2845f03250
13 changed files with 902 additions and 8 deletions

View File

@@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 创建公告输入
/// </summary>
public class AnnouncementCreateInput
{
/// <summary>
/// 标题
/// </summary>
[Required(ErrorMessage = "标题不能为空")]
[StringLength(200, ErrorMessage = "标题不能超过200个字符")]
public string Title { get; set; }
/// <summary>
/// 内容列表
/// </summary>
[Required(ErrorMessage = "内容不能为空")]
[MinLength(1, ErrorMessage = "至少需要一条内容")]
public List<string> Content { get; set; } = new List<string>();
/// <summary>
/// 备注
/// </summary>
[StringLength(500, ErrorMessage = "备注不能超过500个字符")]
public string? Remark { get; set; }
/// <summary>
/// 图片url
/// </summary>
[StringLength(500, ErrorMessage = "图片URL不能超过500个字符")]
public string? ImageUrl { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[Required(ErrorMessage = "开始时间不能为空")]
public DateTime StartTime { get; set; }
/// <summary>
/// 活动结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 公告类型
/// </summary>
[Required(ErrorMessage = "公告类型不能为空")]
public AnnouncementTypeEnum Type { get; set; }
/// <summary>
/// 跳转链接
/// </summary>
[StringLength(500, ErrorMessage = "跳转链接不能超过500个字符")]
public string? Url { get; set; }
}

View File

@@ -0,0 +1,59 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 公告 DTO后台管理使用
/// </summary>
public class AnnouncementDto
{
/// <summary>
/// 公告ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 内容列表
/// </summary>
public List<string> Content { get; set; } = new List<string>();
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 图片url
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 开始时间
/// </summary>
public DateTime StartTime { get; set; }
/// <summary>
/// 活动结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 公告类型
/// </summary>
public AnnouncementTypeEnum Type { get; set; }
/// <summary>
/// 跳转链接
/// </summary>
public string? Url { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
}

View File

@@ -0,0 +1,29 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 获取公告列表输入
/// </summary>
public class AnnouncementGetListInput
{
/// <summary>
/// 搜索关键字
/// </summary>
public string? SearchKey { get; set; }
/// <summary>
/// 跳过数量
/// </summary>
public int SkipCount { get; set; } = 0;
/// <summary>
/// 最大结果数量
/// </summary>
public int MaxResultCount { get; set; } = 10;
/// <summary>
/// 公告类型
/// </summary>
public AnnouncementTypeEnum? Type { get; set; }
}

View File

@@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 更新公告输入
/// </summary>
public class AnnouncementUpdateInput
{
/// <summary>
/// 公告ID
/// </summary>
[Required(ErrorMessage = "公告ID不能为空")]
public Guid Id { get; set; }
/// <summary>
/// 标题
/// </summary>
[Required(ErrorMessage = "标题不能为空")]
[StringLength(200, ErrorMessage = "标题不能超过200个字符")]
public string Title { get; set; }
/// <summary>
/// 内容列表
/// </summary>
[Required(ErrorMessage = "内容不能为空")]
[MinLength(1, ErrorMessage = "至少需要一条内容")]
public List<string> Content { get; set; } = new List<string>();
/// <summary>
/// 备注
/// </summary>
[StringLength(500, ErrorMessage = "备注不能超过500个字符")]
public string? Remark { get; set; }
/// <summary>
/// 图片url
/// </summary>
[StringLength(500, ErrorMessage = "图片URL不能超过500个字符")]
public string? ImageUrl { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[Required(ErrorMessage = "开始时间不能为空")]
public DateTime StartTime { get; set; }
/// <summary>
/// 活动结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 公告类型
/// </summary>
[Required(ErrorMessage = "公告类型不能为空")]
public AnnouncementTypeEnum Type { get; set; }
/// <summary>
/// 跳转链接
/// </summary>
[StringLength(500, ErrorMessage = "跳转链接不能超过500个字符")]
public string? Url { get; set; }
}

View File

@@ -1,3 +1,4 @@
using Volo.Abp.Application.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
@@ -8,8 +9,42 @@ namespace Yi.Framework.AiHub.Application.Contracts.IServices;
public interface IAnnouncementService
{
/// <summary>
/// 获取公告信息
/// 获取公告信息(前端首页使用)
/// </summary>
/// <returns>公告信息</returns>
Task<List<AnnouncementLogDto>> GetAsync();
/// <summary>
/// 获取公告列表(后台管理使用)
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>分页公告列表</returns>
Task<PagedResultDto<AnnouncementDto>> GetListAsync(AnnouncementGetListInput input);
/// <summary>
/// 根据ID获取公告
/// </summary>
/// <param name="id">公告ID</param>
/// <returns>公告详情</returns>
Task<AnnouncementDto> GetByIdAsync(Guid id);
/// <summary>
/// 创建公告
/// </summary>
/// <param name="input">创建输入</param>
/// <returns>创建的公告</returns>
Task<AnnouncementDto> CreateAsync(AnnouncementCreateInput input);
/// <summary>
/// 更新公告
/// </summary>
/// <param name="input">更新输入</param>
/// <returns>更新后的公告</returns>
Task<AnnouncementDto> UpdateAsync(AnnouncementUpdateInput input);
/// <summary>
/// 删除公告
/// </summary>
/// <param name="id">公告ID</param>
Task DeleteAsync(Guid id);
}

View File

@@ -1,6 +1,10 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
@@ -31,8 +35,9 @@ public class AnnouncementService : ApplicationService, IAnnouncementService
}
/// <summary>
/// 获取公告信息
/// 获取公告信息(前端首页使用,允许匿名访问)
/// </summary>
[AllowAnonymous]
public async Task<List<AnnouncementLogDto>> GetAsync()
{
// 使用 GetOrAddAsync 从缓存获取或添加数据缓存1小时
@@ -48,18 +53,124 @@ public class AnnouncementService : ApplicationService, IAnnouncementService
return cacheData?.Logs ?? new List<AnnouncementLogDto>();
}
/// <summary>
/// 获取公告列表(后台管理使用)
/// </summary>
[Authorize(Roles = "admin")]
[HttpGet("announcement/list")]
public async Task<PagedResultDto<AnnouncementDto>> GetListAsync(AnnouncementGetListInput input)
{
var query = _announcementRepository._DbQueryable
.WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey),
x => x.Title.Contains(input.SearchKey!) || (x.Remark != null && x.Remark.Contains(input.SearchKey!)))
.WhereIF(input.Type.HasValue, x => x.Type == input.Type!.Value)
.OrderByDescending(x => x.StartTime);
var totalCount = await query.CountAsync();
var items = await query
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
.ToListAsync();
return new PagedResultDto<AnnouncementDto>(
totalCount,
items.Adapt<List<AnnouncementDto>>()
);
}
/// <summary>
/// 根据ID获取公告
/// </summary>
[Authorize(Roles = "admin")]
[HttpGet("{id}")]
public async Task<AnnouncementDto> GetByIdAsync(Guid id)
{
var entity = await _announcementRepository.GetByIdAsync(id);
if (entity == null)
{
throw new Exception("公告不存在");
}
return entity.Adapt<AnnouncementDto>();
}
/// <summary>
/// 创建公告
/// </summary>
[Authorize(Roles = "admin")]
[HttpPost]
public async Task<AnnouncementDto> CreateAsync(AnnouncementCreateInput input)
{
var entity = input.Adapt<AnnouncementAggregateRoot>();
await _announcementRepository.InsertAsync(entity);
// 清除缓存
await _announcementCache.RemoveAsync(AnnouncementCacheKey);
return entity.Adapt<AnnouncementDto>();
}
/// <summary>
/// 更新公告
/// </summary>
[Authorize(Roles = "admin")]
[HttpPut]
public async Task<AnnouncementDto> UpdateAsync(AnnouncementUpdateInput input)
{
var entity = await _announcementRepository.GetByIdAsync(input.Id);
if (entity == null)
{
throw new Exception("公告不存在");
}
// 更新字段
entity.Title = input.Title;
entity.Content = input.Content;
entity.Remark = input.Remark;
entity.ImageUrl = input.ImageUrl;
entity.StartTime = input.StartTime;
entity.EndTime = input.EndTime;
entity.Type = input.Type;
entity.Url = input.Url;
await _announcementRepository.UpdateAsync(entity);
// 清除缓存
await _announcementCache.RemoveAsync(AnnouncementCacheKey);
return entity.Adapt<AnnouncementDto>();
}
/// <summary>
/// 删除公告
/// </summary>
[Authorize(Roles = "admin")]
[HttpDelete("announcement/{id}")]
public async Task DeleteAsync(Guid id)
{
var entity = await _announcementRepository.GetByIdAsync(id);
if (entity == null)
{
throw new Exception("公告不存在");
}
await _announcementRepository.DeleteAsync(entity);
// 清除缓存
await _announcementCache.RemoveAsync(AnnouncementCacheKey);
}
/// <summary>
/// 从数据库加载公告数据
/// </summary>
private async Task<AnnouncementCacheDto> LoadAnnouncementDataAsync()
{
// 1 一次性查出全部公告(不排序)
// 一次性查出全部公告(不排序)
var logs = await _announcementRepository._DbQueryable
.ToListAsync();
var now = DateTime.Now;
// 2 内存中处理排序
// 内存中处理排序
var orderedLogs = logs
.OrderByDescending(x =>
x.StartTime <= now &&