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 或不一致的地方,建议修复:

  1. 类的声明语法

    • 代码使用了类似 C# primary constructor 的写法(public class CommunityController(... ) : ControllerBase),这是 C# 的较新语法(C# 12 preview 或类似扩展)。如果项目没有启用该语法会编译失败。若不使用该语法,请改为常规构造函数注入。
  2. 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 天的数据)。
  3. SendMessage 中的数组/参数写法问题(DEBUG 区域)

    • await _connection.InvokeCoreAsync("SendSystemNotification", [message]); 这不是有效 C# 语法。应该使用 new object[] { message } 或者 InvokeAsync 并传递参数数组。
    • 缺少重试/错误处理,连接可能失败。
  4. 路由与返回类型声明不一致或说明不清

    • 有些 ProducesResponseType 标注与实际返回不一致(例如 GetRobots 标注 ProducesResponseType(StatusCodes.Status204NoContent) 但方法返回 Ok(robots))。建议使文档注解与实际返回一致。
    • 对需要授权的方法,应在注释中说明权限和作用范围,便于维护。
  5. 命名与拼写错误

    • SvaeRobotsRequest 可能是拼写错误,应该为 SaveRobotsRequest 或 SaveRobotsDto。
    • CacheKey.BannerKey 的来源未在这段代码显示,确保它存在且命名规范。
  6. 异常处理与空值防护

    • 当前方法直接 await 服务调用并返回结果,缺少 try/catch 或对 null 的明确处理(例如 pictureRecordService.GetBannerAsync 返回 null)。建议在关键点加上合适的错误处理或返回 500/400 的友好消息。
  7. 安全性考虑

    • robots、footer 的读写接口在代码里对保存操作做了授权,但 GetRobots 有 Authorize,因此读取也受限——请确认意图(通常 robots.txt 应该公开读取,不需要授权;而保存操作应受限)。
  8. 日志格式化、时间转换

    • GetTop100LogsAsync 中 Convert.ToDateTime(x.Timestamp) 依赖 x.Timestamp 的格式/时区,建议明确时区并处理可能的解析错误。

总结(建议)

  • 功能上这个控制器实现了社区页面相关的一组 API,涵盖缓存、统计、配置项读取/写入、以及调试用的通知发送。
  • 需要修复语法和拼写错误,确认类型一致性(尤其是 SetRandomData);检查并统一 ProducesResponseType 注解与实际返回;根据项目 C# 语言版本决定是否使用 primary constructor 语法或改为常规构造注入。
  • 补充必要的异常处理与输入校验,调整授权策略以符合安全与公开资源的预期(例如 robots.txt 通常无需授权读取)。

如果你需要,我可以:

  • 帮你把有问题的函数(比如 SetRandomData、SendMessage)改成可编译、类型正确的实现;
  • 或者把整个类改写成传统构造器注入形式以保证兼容性。需要哪种帮助请告诉我。
评论加载中...