using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Service.Mediator.Features.Article.Queries;
using Dpz.Core.Service.ObjectStorage.Services;
using Dpz.Core.Web.Library.Api;

namespace Dpz.Core.Web.Controllers;

public partial class ArticleController(
    IArticleService articleService,
    IObjectStorageOperation objectStorageService,
    IMediator mediator,
    ILogger<ArticleController> logger
) : Controller
{
    public async Task<IActionResult> Index(
        [FromServices] IHomeCacheService cacheService,
        string? tag = "",
        [Range(10, 100)] int pageSize = 10,
        int pageIndex = 1,
        CancellationToken cancellationToken = default
    )
    {
        if (!ModelState.IsValid)
        {
            return NotFound();
        }

        var tags = string.IsNullOrEmpty(tag) ? [] : new[] { tag };
        this.SetTitle(string.IsNullOrEmpty(tag) ? "文章列表" : $"标签-{tag}-文章列表");
        var list = await articleService.GetPagesAsync(
            pageIndex,
            pageSize,
            tags: tags,
            cancellationToken: cancellationToken
        );
        if (Request.Headers["X-PJAX"] == "true" && Request.Query["_pjax"] == "#article-list")
        {
            return PartialView("_ArticleListPartial", list);
        }

        var model = new ArticleIndexModel
        {
            LikeArticle = await cacheService.GetRandomArticlesAsync(cancellationToken),
            List = list,
            News = await cacheService.GetLatestArticlesAsync(cancellationToken),
            Tags = await cacheService.GetArticleTagsAsync(cancellationToken),
        };
        return View(model);
    }

    public async Task<IActionResult> Read(
        string id = "",
        string text = "",
        CancellationToken cancellationToken = default
    )
    {
        var article = await mediator.Send(
            new ArticleReadRequest { Id = id, Text = text },
            cancellationToken
        );
        if (article == null)
        {
            return NotFound();
        }

        await articleService.ViewAsync(id, cancellationToken);
        this.SetTitle(article.Title);

        var pageMetaPage = new VmPageMetadata
        {
            Description = ClearIntroductionRegex().Replace(article.Introduction ?? "", ""),
            Keywords = [article.Title],
            Relations = ["Article", "Read", id],
        };
        ViewData["PageMetadata"] = pageMetaPage;
        ViewData["Text"] = text;
        return View(article);
    }

    [CheckAuthorize]
    [HttpPost]
    public async Task<IActionResult> Upload(CancellationToken cancellationToken)
    {
        var file = Request.Form.Files.FirstOrDefault();
        if (file is { Length: > 0 } && file.ContentType.Contains("image"))
        {
            //var userInfo = User.GetIdentity();
            var path = new[] { "images", "article", DateTime.Now.ToString("yyyy-MM-dd") };
            var fileName =
                ObjectId.GenerateNewId() + file.FileName[file.FileName.LastIndexOf('.')..];
            var result = await objectStorageService.UploadAsync(
                file.OpenReadStream(),
                path,
                fileName,
                cancellationToken
            );
            return Json(
                new
                {
                    success = 1,
                    message = "",
                    url = result.AccessUrl,
                }
            );
        }

        return Json(
            new
            {
                success = 0,
                message = "请选择一张图片!",
                url = "",
            }
        );
    }

    [HttpPost, CheckAuthorize]
    public async Task<IActionResult> Publish(
        EditArticleRequest model,
        string newTag = "",
        CancellationToken cancellationToken = default
    )
    {
        var userInfo = User.RequiredUserInfo;

        var checkResult = ModelState.CheckModelState();

        if (checkResult.IsValid)
        {
            return Json(new ResultInfo(string.Join("\n", checkResult.ErrorMessages)));
        }

        if (!string.IsNullOrWhiteSpace(newTag))
        {
            var newTags = newTag.Split(',').Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
            model.Tags.AddRange(newTags);
        }

        // 编辑
        if (!string.IsNullOrEmpty(model.Id))
        {
            var article = await articleService.GetArticleAsync(model.Id, cancellationToken);
            if (article == null)
            {
                return Json(new ResultInfo("文章不存在"));
            }

            if (userInfo.Id != article.Author?.Id)
            {
                return Json(new ResultInfo("不能修改别人发布的文章!"));
            }

            await articleService.EditArticleAsync(model, userInfo, cancellationToken);
            return Json(new ResultInfo(data: model.Id));
        }

        try
        {
            // 新增
            var createdArticle = await articleService.CreateArticleAsync(
                model,
                userInfo,
                cancellationToken
            );
            await articleService.ClearCacheAsync(cancellationToken);
            return Json(new ResultInfo(data: createdArticle.Id));
        }
        catch (Exception ex)
        {
            logger.LogInformation(ex, "创建文章失败");
            return Json(ResultInfo.ToFail(ex.Message));
        }
    }

    [CheckAuthorize, HttpPost]
    public async Task<IActionResult> Delete(string id, CancellationToken cancellationToken)
    {
        var article = await articleService.GetArticleAsync(id, cancellationToken);
        if (article == null || User.RequiredUserId != article.Author?.Id)
        {
            return Json(new ResultInfo("未找到文章,或者不能删除别人发布的文章!"));
        }

        await articleService.DeleteAsync(id, cancellationToken);
        return Json(new ResultInfo(true));
    }

    [HttpGet]
    public async Task<IActionResult> Search(
        string? keyword = "",
        CancellationToken cancellationToken = default
    )
    {
        var request = new ArticleSearchRequest { Keyword = keyword };
        var searchResult = await mediator.Send(request, cancellationToken);

        var tags = await articleService.GetAllTagsAsync(cancellationToken);
        var model = new ArticleSearchModel(keyword, searchResult, tags);
        this.SetTitle($"搜索 - {keyword}");
        return View(model);
    }

    [GeneratedRegex("<[^>]*>")]
    private static partial Regex ClearIntroductionRegex();
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个ASP.NET Core MVC的文章管理控制器代码,我来为您详细解释每个部分的功能:

控制器概述

ArticleController 是一个处理文章相关操作的控制器,使用了现代C#的主构造函数语法,注入了以下依赖服务:

  • IArticleService: 文章业务逻辑服务
  • IObjectStorageOperation: 对象存储服务(用于文件上传)
  • IMediator: 中介者模式服务(处理查询请求)
  • ILogger: 日志服务

主要功能方法

1. Index 方法 - 文章列表页面

public async Task<IActionResult> Index(...)
  • 支持按标签筛选文章
  • 分页显示文章列表(页面大小10-100可配置)
  • 支持PJAX异步加载(提升用户体验)
  • 返回包含推荐文章、最新文章、标签等的完整页面模型

2. Read 方法 - 文章阅读页面

public async Task<IActionResult> Read(...)
  • 根据ID和文本参数获取文章详情
  • 增加文章浏览量
  • 设置页面SEO元数据(标题、描述、关键词)
  • 使用正则表达式清理文章简介中的HTML标签

3. Upload 方法 - 图片上传

[CheckAuthorize]
[HttpPost]
public async Task<IActionResult> Upload(...)
  • 需要用户授权认证
  • 仅支持图片文件上传
  • 按日期组织存储路径(images/article/yyyy-MM-dd)
  • 使用ObjectId生成唯一文件名
  • 返回JSON格式的上传结果

4. Publish 方法 - 发布/编辑文章

[HttpPost, CheckAuthorize]
public async Task<IActionResult> Publish(...)
  • 需要用户授权认证
  • 支持新增和编辑两种模式
  • 处理新标签的添加
  • 验证用户权限(只能编辑自己的文章)
  • 包含完整的错误处理和日志记录

5. Delete 方法 - 删除文章

[CheckAuthorize, HttpPost]
public async Task<IActionResult> Delete(...)
  • 需要用户授权认证
  • 验证文章存在性和用户权限
  • 只允许删除自己发布的文章
[HttpGet]
public async Task<IActionResult> Search(...)
  • 根据关键词搜索文章
  • 使用中介者模式处理搜索请求
  • 返回搜索结果和所有标签信息

技术特点

  1. 异步编程: 所有方法都使用 async/await 模式
  2. 取消令牌: 支持请求取消,提高系统响应性
  3. 数据验证: 使用 DataAnnotations 进行参数验证
  4. 权限控制: 通过 CheckAuthorize 特性控制访问权限
  5. SEO优化: 动态设置页面标题和元数据
  6. PJAX支持: 提供部分页面更新功能
  7. 错误处理: 完善的异常处理和用户友好的错误信息

这个控制器设计得很规范,遵循了现代Web开发的最佳实践,具有良好的可维护性和扩展性。

评论加载中...