using Dpz.Core.Public.ViewModel.Response;
namespace Dpz.Core.WebApi.Controllers;
/// <summary>
/// 其他的
/// </summary>
[ApiController, Route("api/[controller]")]
public class CommunityController(
IPictureRecordService pictureRecordService,
IArticleService articleService,
IAppOptionService appOptionService,
IAppLogEntryService logEntryService,
IFusionCache fusionCache,
IConfiguration configuration
) : ControllerBase
{
/// <summary>
/// 获取banner
/// </summary>
/// <returns></returns>
[HttpGet("getBanners")]
[ProducesResponseType<List<PictureRecordResponse>>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetBanners()
{
var banners = await GetBannerAsync();
return Ok(banners);
}
/// <summary>
/// 获取汇总信息
/// </summary>
/// <returns></returns>
[HttpGet("summary")]
[Authorize(Policy = "System")]
[ProducesResponseType<SummaryInformation>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetSummary()
{
const string summaryKey = "community-summary";
var cache = await fusionCache.TryGetAsync<SummaryInformation>(summaryKey);
if (cache.HasValue)
{
return Ok(cache.Value);
}
var banner = await GetBannerAsync();
var summary = new SummaryInformation
{
ArticleTotalCount = await articleService.GetTotalCountAsync(),
Banner = banner,
LatestArticles = await articleService.GetLatestAsync(),
TodayArticleCount = await articleService.GetTodayCountAsync(),
};
if (
configuration["AgileConfig:env"]?.Equals("PROD", StringComparison.OrdinalIgnoreCase)
== false
)
{
SetRandomData(summary);
}
else
{
summary.LatestLogs = await GetTop100LogsAsync();
summary.TodayAccessNumber = await logEntryService.GetLatestAccessNumberAsync();
summary.WeekAccessNumber = await logEntryService.GetLatestAccessNumberAsync(-7);
}
await fusionCache.SetAsync(summaryKey, summary, TimeSpan.FromHours(3));
return Ok(summary);
}
[NonAction]
private static void SetRandomData(SummaryInformation summary)
{
summary.TodayAccessNumber = [];
summary.WeekAccessNumber = [];
summary.LatestLogs = $"[{DateTime.Now:HH:mm:ss} INF] this is test log";
var random = new Random();
for (var i = 0; i < 7; i++)
{
if (i == 0)
{
summary.TodayAccessNumber.Add(
new AccessSummary
{
Date = DateTime.Now.ToString("yyyy/MM/dd"),
Count = random.Next(100, 100000),
}
);
}
summary.WeekAccessNumber.Add(
new AccessSummary
{
Date = DateTime.Now.AddDays(-(i + 1)).ToString("yyyy/MM/dd"),
Count = random.Next(100, 100000),
}
);
}
}
private async Task<List<PictureRecordResponse>> GetBannerAsync()
{
var cache = await fusionCache.TryGetAsync<List<PictureRecordResponse>>(CacheKey.BannerKey);
if (cache.HasValue)
{
return cache.Value;
}
var banner = await pictureRecordService.GetBannerAsync();
await fusionCache.SetAsync(CacheKey.BannerKey, banner, TimeSpan.FromHours(12));
return banner.ToList();
}
[NonAction]
private async Task<string> GetTop100LogsAsync()
{
var logs = await logEntryService.GetPageAsync(pageSize: 100);
return string.Join(
"\n",
logs?.Events.Select(x =>
{
var recordTime = Convert.ToDateTime(x.Timestamp);
return $"[{recordTime:HH:mm:ss} {x.Level}] {x.RenderedMessage}";
}) ?? new List<string>()
);
}
/// <summary>
/// 获取壁纸
/// </summary>
/// <param name="bingWallpaper"></param>
/// <returns></returns>
[HttpGet("wallpaper")]
public async Task<ActionResult<List<Wallpaper>>> Wallpaper(
[FromServices] IBingWallpaper bingWallpaper
)
{
var wallpapers = await bingWallpaper.GetTodayWallpapersAsync();
return Ok(wallpapers);
}
/// <summary>
/// 获取页脚内容
/// </summary>
/// <returns></returns>
[HttpGet("footer")]
[ProducesResponseType<string>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetFooter()
{
var content = await appOptionService.GetFooterContentAsync();
return Ok(content);
}
/// <summary>
/// 保存页脚内容
/// </summary>
/// <param name="footer"></param>
/// <returns></returns>
[HttpPost("footer"), Authorize(Policy = "System")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SaveFooter([FromBody] SaveFooterDto footer)
{
await appOptionService.SaveFooterContentAsync(footer.Content);
return NoContent();
}
/// <summary>
/// 获取robots.txt
/// </summary>
/// <returns></returns>
[HttpGet("robots.txt"), Authorize(Policy = "System")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetRobots()
{
var robots = await appOptionService.GetRobotsAsync();
return Ok(robots);
}
/// <summary>
/// 保存robots.txt
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("robots.txt"), Authorize(Policy = "System")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SaveRobots([FromBody] SvaeRobotsRequest request)
{
await appOptionService.SaveRobotsAsync(request.Content);
return NoContent();
}
/// <summary>
/// 获取友链
/// </summary>
/// <returns></returns>
[HttpGet("friends")]
public async Task<ActionResult<List<VmFriends>>> GetFriends()
{
var friends = await appOptionService.GetFriendsAsync();
return Ok(friends);
}
#if DEBUG
private static HubConnection? _connection;
/// <summary>
///
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
[HttpPost("send")]
public async Task<IActionResult> SendMessage([FromBody] string message)
{
if (_connection == null || _connection.State == HubConnectionState.Disconnected)
{
_connection = new HubConnectionBuilder()
.WithUrl("https://localhost:37701/notification")
.WithAutomaticReconnect()
.Build();
await _connection.StartAsync();
}
await _connection.InvokeCoreAsync("SendSystemNotification", [message]);
return NoContent();
}
#endif
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面是对这段 CommunityController 代码的逐条解释(中文),包括它的功能、各路由的行为、使用到的服务、缓存策略以及我发现的潜在问题和改进建议。
概述
- 这个控制器负责“社区”相关的 API(banner、汇总信息、壁纸、页脚、robots、友链等)。
- 通过依赖注入接收若干后端服务:IPictureRecordService、IArticleService、IAppOptionService、IAppLogEntryService、IFusionCache、IConfiguration 等,用来读取数据并做缓存。
- 一些接口需要授权策略 Policy = "System"(受限管理操作)。
依赖(构造参数)
- pictureRecordService:获取图片记录(banner)。
- articleService:获取文章相关统计、最新文章等。
- appOptionService:读取/保存应用配置(footer、robots、friends 等)。
- logEntryService:读取日志条目与访问数。
- fusionCache:缓存(IFusionCache,缓存 banner、summary 等)。
- configuration:读取配置(用来判断是否为生产环境等)。
主要路由与行为
GET api/Community/getBanners
- 返回 Banner 列表(PictureRecordResponse)。
- 通过 GetBannerAsync 使用 fusionCache(缓存键 CacheKey.BannerKey,缓存 12 小时)。
GET api/Community/summary
- 需要 Authorization(Policy = "System")。
- 返回 SummaryInformation(包含文章总数、banner、最新文章、今日文章数、最新日志、今日/周访问数等)。
- 先尝试从 fusionCache 读取(缓存键 "community-summary"),如果存在直接返回缓存值。
- 否则从 articleService、pictureRecordService、logEntryService 读取数据并构造 summary,再将结果缓存 3 小时。
- 如果配置 AgileConfig:env ≠ "PROD"(非生产),会调用 SetRandomData 生成测试用的随机/假的访问数和日志;否则从日志服务读取真实数据。
GET api/Community/wallpaper
- 不需要授权(但通过参数注入 IBingWallpaper 服务)。
- 返回今日壁纸列表(调用 bingWallpaper.GetTodayWallpapersAsync)。
GET api/Community/footer
- 返回页脚内容(从 appOptionService.GetFooterContentAsync 读取)。
POST api/Community/footer
- 需要 Authorization(Policy = "System")。
- 保存页脚内容(调用 appOptionService.SaveFooterContentAsync),成功返回 NoContent()(204)。
GET api/Community/robots.txt
- 方法上有 Authorize(Policy = "System")(需要授权)。
- 返回 robots 内容(从 appOptionService.GetRobotsAsync 读取)。
POST api/Community/robots.txt
- 需要授权,保存 robots 内容(调用 appOptionService.SaveRobotsAsync),返回 NoContent()。
GET api/Community/friends
- 返回友链(从 appOptionService.GetFriendsAsync)。
调试专用(仅在 DEBUG 下编译)
- POST api/Community/send
- 使用 SignalR 客户端 HubConnection 连接到 https://localhost:37701/notification,然后调用 SendSystemNotification,发送系统通知(主要用于本地调试)。
内部辅助方法
GetBannerAsync()
- 尝试从缓存读取 banner 列表;如果无缓存则询问 pictureRecordService 获取并缓存 12 小时,然后返回。
GetTop100LogsAsync()
- 从 logEntryService 获取最近 100 条日志页,格式化为行文本 "[HH:mm:ss LEVEL] message" 的字符串并用换行连接,返回单一字符串。
SetRandomData(SummaryInformation summary)
- 在非生产环境下,用随机数填充 TodayAccessNumber、WeekAccessNumber、LatestLogs 等以便前端展示测试数据(注意:这个函数里有实现问题,见下面“问题与建议”部分)。
缓存策略
- summary 缓存 3 小时(键 "community-summary")。
- banner 缓存 12 小时(CacheKey.BannerKey)。
- 使用 IFusionCache 的 TryGetAsync / SetAsync 方法(FusionCache 是一种分布式/本地缓存抽象)。
代码问题、潜在缺陷与改进建议 我在代码里发现若干错误、潜在 bug 或不一致的地方,建议修复:
类的声明语法
- 代码使用了类似 C# primary constructor 的写法(public class CommunityController(... ) : ControllerBase),这是 C# 的较新语法(C# 12 preview 或类似扩展)。如果项目没有启用该语法会编译失败。若不使用该语法,请改为常规构造函数注入。
SetRandomData 实现中存在语法/类型错误
- summary.TodayAccessNumber = []; 这不是有效的 C#,应为 new List
()。 - summary.WeekAccessNumber = []; 同样应初始化为 List。
- summary.LatestLogs = $"[{DateTime.Now:HH:mm:ss} INF] this is test log"; 如果 LatestLogs 的类型不是 string(比如是 List
或者某种对象),这里会类型不匹配。请确认 SummaryInformation 的属性类型并按类型赋值(例如 LatestLogs 为 string 时 OK;若为 List 则应 Add)。 - 循环中 i0 时把 TodayAccessNumber.Add(...) 放在 i0 条件里,但 WeekAccessNumber 每次都添加。请确认业务意图(Today 应该只留今天的数据,Week 应该有最近 7 天的数据)。
- summary.TodayAccessNumber = []; 这不是有效的 C#,应为 new List
SendMessage 中的数组/参数写法问题(DEBUG 区域)
- await _connection.InvokeCoreAsync("SendSystemNotification", [message]); 这不是有效 C# 语法。应该使用 new object[] { message } 或者 InvokeAsync 并传递参数数组。
- 缺少重试/错误处理,连接可能失败。
路由与返回类型声明不一致或说明不清
- 有些 ProducesResponseType 标注与实际返回不一致(例如 GetRobots 标注 ProducesResponseType(StatusCodes.Status204NoContent) 但方法返回 Ok(robots))。建议使文档注解与实际返回一致。
- 对需要授权的方法,应在注释中说明权限和作用范围,便于维护。
命名与拼写错误
- SvaeRobotsRequest 可能是拼写错误,应该为 SaveRobotsRequest 或 SaveRobotsDto。
- CacheKey.BannerKey 的来源未在这段代码显示,确保它存在且命名规范。
异常处理与空值防护
- 当前方法直接 await 服务调用并返回结果,缺少 try/catch 或对 null 的明确处理(例如 pictureRecordService.GetBannerAsync 返回 null)。建议在关键点加上合适的错误处理或返回 500/400 的友好消息。
安全性考虑
- robots、footer 的读写接口在代码里对保存操作做了授权,但 GetRobots 有 Authorize,因此读取也受限——请确认意图(通常 robots.txt 应该公开读取,不需要授权;而保存操作应受限)。
日志格式化、时间转换
- GetTop100LogsAsync 中 Convert.ToDateTime(x.Timestamp) 依赖 x.Timestamp 的格式/时区,建议明确时区并处理可能的解析错误。
总结(建议)
- 功能上这个控制器实现了社区页面相关的一组 API,涵盖缓存、统计、配置项读取/写入、以及调试用的通知发送。
- 需要修复语法和拼写错误,确认类型一致性(尤其是 SetRandomData);检查并统一 ProducesResponseType 注解与实际返回;根据项目 C# 语言版本决定是否使用 primary constructor 语法或改为常规构造注入。
- 补充必要的异常处理与输入校验,调整授权策略以符合安全与公开资源的预期(例如 robots.txt 通常无需授权读取)。
如果你需要,我可以:
- 帮你把有问题的函数(比如 SetRandomData、SendMessage)改成可编译、类型正确的实现;
- 或者把整个类改写成传统构造器注入形式以保证兼容性。需要哪种帮助请告诉我。
评论加载中...