diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/Articles/ArticleService.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/Articles/ArticleService.cs new file mode 100644 index 0000000..bcb8455 --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/Articles/ArticleService.cs @@ -0,0 +1,97 @@ +using DotNetGuideBlogModel.Dtos; +using DotNetGuideBlogModel.Entities; +using DotNetGuideBlogRepository.Articles; + +namespace DotNetGuideBlogBLL.Articles; + +public class ArticleService(IArticleRepository articleRepository) : IArticleService +{ + public async Task> GetListAsync() + { + var articles = await articleRepository.GetListAsync(); + return articles.Select(MapToDto).ToList(); + } + + public async Task GetByIdAsync(int id) + { + if (id <= 0) + { + return null; + } + + var article = await articleRepository.GetByIdAsync(id); + return article is null ? null : MapToDto(article); + } + + public async Task AddAsync(CreateArticleRequest request) + { + if (!IsValidTitle(request.Title)) + { + return null; + } + + var entity = new Article + { + Title = request.Title.Trim(), + Content = request.Content?.Trim() ?? string.Empty, + Author = request.Author?.Trim() ?? string.Empty + }; + + var created = await articleRepository.AddAsync(entity); + return MapToDto(created); + } + + public async Task UpdateAsync(int id, UpdateArticleRequest request) + { + if (id <= 0 || !IsValidTitle(request.Title)) + { + return false; + } + + var existing = await articleRepository.GetByIdAsync(id); + if (existing is null) + { + return false; + } + + existing.Title = request.Title.Trim(); + existing.Content = request.Content?.Trim() ?? string.Empty; + existing.Author = request.Author?.Trim() ?? string.Empty; + + return await articleRepository.UpdateAsync(existing); + } + + public async Task DeleteAsync(int id) + { + if (id <= 0) + { + return false; + } + + var existing = await articleRepository.GetByIdAsync(id); + if (existing is null) + { + return false; + } + + return await articleRepository.DeleteAsync(id); + } + + private static bool IsValidTitle(string? title) + { + return !string.IsNullOrWhiteSpace(title); + } + + private static ArticleDto MapToDto(Article article) + { + return new ArticleDto + { + Id = article.Id, + Title = article.Title, + Content = article.Content, + Author = article.Author, + CreateTime = article.CreateTime, + UpdateTime = article.UpdateTime + }; + } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/Articles/IArticleService.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/Articles/IArticleService.cs new file mode 100644 index 0000000..b32c84f --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/Articles/IArticleService.cs @@ -0,0 +1,16 @@ +using DotNetGuideBlogModel.Dtos; + +namespace DotNetGuideBlogBLL.Articles; + +public interface IArticleService +{ + Task> GetListAsync(); + + Task GetByIdAsync(int id); + + Task AddAsync(CreateArticleRequest request); + + Task UpdateAsync(int id, UpdateArticleRequest request); + + Task DeleteAsync(int id); +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/DotNetGuideBlogBLL.csproj b/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/DotNetGuideBlogBLL.csproj index fa71b7a..0311adc 100644 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/DotNetGuideBlogBLL.csproj +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogBLL/DotNetGuideBlogBLL.csproj @@ -6,4 +6,9 @@ enable + + + + + diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/Articles/ArticleDal.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/Articles/ArticleDal.cs new file mode 100644 index 0000000..a992f5b --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/Articles/ArticleDal.cs @@ -0,0 +1,94 @@ +using System.Collections.Concurrent; +using System.Threading; +using DotNetGuideBlogModel.Entities; + +namespace DotNetGuideBlogDAL.Articles; + +/// +/// 使用线程安全内存集合模拟数据库访问。 +/// +public class ArticleDal : IArticleDal +{ + private readonly ConcurrentDictionary _articles = new(); + private int _idSeed; + + public Task> GetListAsync() + { + var result = _articles.Values + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.CreateTime) + .Select(Clone) + .ToList(); + + return Task.FromResult(result); + } + + public Task GetByIdAsync(int id) + { + if (_articles.TryGetValue(id, out var article) && !article.IsDeleted) + { + return Task.FromResult(Clone(article)); + } + + return Task.FromResult(null); + } + + public Task
AddAsync(Article article) + { + var now = DateTime.UtcNow; + var entity = Clone(article); + entity.Id = Interlocked.Increment(ref _idSeed); + entity.CreateTime = now; + entity.UpdateTime = null; + entity.IsDeleted = false; + + _articles[entity.Id] = entity; + return Task.FromResult(Clone(entity)); + } + + public Task UpdateAsync(Article article) + { + if (!_articles.TryGetValue(article.Id, out var existing) || existing.IsDeleted) + { + return Task.FromResult(false); + } + + var updated = Clone(existing); + updated.Title = article.Title; + updated.Content = article.Content; + updated.Author = article.Author; + updated.UpdateTime = DateTime.UtcNow; + + _articles[article.Id] = updated; + return Task.FromResult(true); + } + + public Task DeleteAsync(int id) + { + if (!_articles.TryGetValue(id, out var existing) || existing.IsDeleted) + { + return Task.FromResult(false); + } + + var deleted = Clone(existing); + deleted.IsDeleted = true; + deleted.UpdateTime = DateTime.UtcNow; + + _articles[id] = deleted; + return Task.FromResult(true); + } + + private static Article Clone(Article source) + { + return new Article + { + Id = source.Id, + Title = source.Title, + Content = source.Content, + Author = source.Author, + CreateTime = source.CreateTime, + UpdateTime = source.UpdateTime, + IsDeleted = source.IsDeleted + }; + } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/Articles/IArticleDal.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/Articles/IArticleDal.cs new file mode 100644 index 0000000..0f17f4c --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/Articles/IArticleDal.cs @@ -0,0 +1,16 @@ +using DotNetGuideBlogModel.Entities; + +namespace DotNetGuideBlogDAL.Articles; + +public interface IArticleDal +{ + Task> GetListAsync(); + + Task GetByIdAsync(int id); + + Task
AddAsync(Article article); + + Task UpdateAsync(Article article); + + Task DeleteAsync(int id); +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/DotNetGuideBlogDAL.csproj b/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/DotNetGuideBlogDAL.csproj index fa71b7a..f1f7422 100644 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/DotNetGuideBlogDAL.csproj +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogDAL/DotNetGuideBlogDAL.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Common/ApiResult.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Common/ApiResult.cs new file mode 100644 index 0000000..d552e2b --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Common/ApiResult.cs @@ -0,0 +1,30 @@ +namespace DotNetGuideBlogModel.Common; + +public class ApiResult +{ + public bool Success { get; set; } + + public string Message { get; set; } = string.Empty; + + public T? Data { get; set; } + + public static ApiResult Ok(T? data, string message = "操作成功") + { + return new ApiResult + { + Success = true, + Message = message, + Data = data + }; + } + + public static ApiResult Fail(string message) + { + return new ApiResult + { + Success = false, + Message = message, + Data = default + }; + } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/ArticleDto.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/ArticleDto.cs new file mode 100644 index 0000000..b902401 --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/ArticleDto.cs @@ -0,0 +1,16 @@ +namespace DotNetGuideBlogModel.Dtos; + +public class ArticleDto +{ + public int Id { get; set; } + + public string Title { get; set; } = string.Empty; + + public string Content { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; + + public DateTime CreateTime { get; set; } + + public DateTime? UpdateTime { get; set; } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/CreateArticleRequest.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/CreateArticleRequest.cs new file mode 100644 index 0000000..603dce5 --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/CreateArticleRequest.cs @@ -0,0 +1,10 @@ +namespace DotNetGuideBlogModel.Dtos; + +public class CreateArticleRequest +{ + public string Title { get; set; } = string.Empty; + + public string Content { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/UpdateArticleRequest.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/UpdateArticleRequest.cs new file mode 100644 index 0000000..c742160 --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Dtos/UpdateArticleRequest.cs @@ -0,0 +1,10 @@ +namespace DotNetGuideBlogModel.Dtos; + +public class UpdateArticleRequest +{ + public string Title { get; set; } = string.Empty; + + public string Content { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Entities/Article.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Entities/Article.cs new file mode 100644 index 0000000..8592f08 --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogModel/Entities/Article.cs @@ -0,0 +1,18 @@ +namespace DotNetGuideBlogModel.Entities; + +public class Article +{ + public int Id { get; set; } + + public string Title { get; set; } = string.Empty; + + public string Content { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; + + public DateTime CreateTime { get; set; } + + public DateTime? UpdateTime { get; set; } + + public bool IsDeleted { get; set; } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/Articles/ArticleRepository.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/Articles/ArticleRepository.cs new file mode 100644 index 0000000..236b91f --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/Articles/ArticleRepository.cs @@ -0,0 +1,32 @@ +using DotNetGuideBlogDAL.Articles; +using DotNetGuideBlogModel.Entities; + +namespace DotNetGuideBlogRepository.Articles; + +public class ArticleRepository(IArticleDal articleDal) : IArticleRepository +{ + public Task GetByIdAsync(int id) + { + return articleDal.GetByIdAsync(id); + } + + public Task> GetListAsync() + { + return articleDal.GetListAsync(); + } + + public Task
AddAsync(Article article) + { + return articleDal.AddAsync(article); + } + + public Task UpdateAsync(Article article) + { + return articleDal.UpdateAsync(article); + } + + public Task DeleteAsync(int id) + { + return articleDal.DeleteAsync(id); + } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/Articles/IArticleRepository.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/Articles/IArticleRepository.cs new file mode 100644 index 0000000..2988f9f --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/Articles/IArticleRepository.cs @@ -0,0 +1,16 @@ +using DotNetGuideBlogModel.Entities; + +namespace DotNetGuideBlogRepository.Articles; + +public interface IArticleRepository +{ + Task GetByIdAsync(int id); + + Task> GetListAsync(); + + Task
AddAsync(Article article); + + Task UpdateAsync(Article article); + + Task DeleteAsync(int id); +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/DotNetGuideBlogRepository.csproj b/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/DotNetGuideBlogRepository.csproj index fa71b7a..32dd40c 100644 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/DotNetGuideBlogRepository.csproj +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogRepository/DotNetGuideBlogRepository.csproj @@ -6,4 +6,9 @@ enable + + + + + diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Controllers/ArticleController.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Controllers/ArticleController.cs new file mode 100644 index 0000000..5b275e5 --- /dev/null +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Controllers/ArticleController.cs @@ -0,0 +1,54 @@ +using DotNetGuideBlogBLL.Articles; +using DotNetGuideBlogModel.Common; +using DotNetGuideBlogModel.Dtos; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetGuideBlogWebAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ArticleController(IArticleService articleService) : ControllerBase +{ + [HttpGet] + public async Task>> GetListAsync() + { + var result = await articleService.GetListAsync(); + return ApiResult>.Ok(result); + } + + [HttpGet("{id:int}")] + public async Task> GetByIdAsync(int id) + { + var result = await articleService.GetByIdAsync(id); + return result is null + ? ApiResult.Fail("文章不存在") + : ApiResult.Ok(result); + } + + [HttpPost] + public async Task> CreateAsync([FromBody] CreateArticleRequest request) + { + var result = await articleService.AddAsync(request); + return result is null + ? ApiResult.Fail("标题不能为空") + : ApiResult.Ok(result, "创建成功"); + } + + [HttpPut("{id:int}")] + public async Task> UpdateAsync(int id, [FromBody] UpdateArticleRequest request) + { + var success = await articleService.UpdateAsync(id, request); + return success + ? ApiResult.Ok(true, "更新成功") + : ApiResult.Fail("更新失败,文章不存在或标题无效"); + } + + [HttpDelete("{id:int}")] + public async Task> DeleteAsync(int id) + { + var success = await articleService.DeleteAsync(id); + return success + ? ApiResult.Ok(true, "删除成功") + : ApiResult.Fail("删除失败,文章不存在"); + } +} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Controllers/WeatherForecastController.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Controllers/WeatherForecastController.cs deleted file mode 100644 index b9da5e5..0000000 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace DotNetGuideBlogWebAPI.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/DotNetGuideBlogWebAPI.csproj b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/DotNetGuideBlogWebAPI.csproj index 1b28a01..ddd385d 100644 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/DotNetGuideBlogWebAPI.csproj +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/DotNetGuideBlogWebAPI.csproj @@ -6,4 +6,13 @@ enable + + + + + + + + + diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Program.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Program.cs index 2c05560..f805ad8 100644 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Program.cs +++ b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/Program.cs @@ -1,25 +1,35 @@ -namespace DotNetGuideBlogWebAPI -{ - public class Program - { - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); - - // Add services to the container. - - builder.Services.AddControllers(); +using DotNetGuideBlogBLL.Articles; +using DotNetGuideBlogDAL.Articles; +using DotNetGuideBlogRepository.Articles; - var app = builder.Build(); +namespace DotNetGuideBlogWebAPI; - // Configure the HTTP request pipeline. +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); - app.UseAuthorization(); + // 注册控制器与 Swagger。 + builder.Services.AddControllers(); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + // 注册分层依赖,内存 DAL 使用单例共享数据。 + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); - app.MapControllers(); + var app = builder.Build(); - app.Run(); + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); } + + app.UseAuthorization(); + app.MapControllers(); + app.Run(); } } diff --git a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/WeatherForecast.cs b/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/WeatherForecast.cs deleted file mode 100644 index c0f36f3..0000000 --- a/Platforms/DotNetGuideBlog/DotNetGuideBlogWebAPI/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DotNetGuideBlogWebAPI -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -}