diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..0582245 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/StarBlog.Api/Apis/Admin/ConfigController.cs b/src/StarBlog.Api/Apis/Admin/ConfigController.cs index 51d5b58..ccb6a73 100644 --- a/src/StarBlog.Api/Apis/Admin/ConfigController.cs +++ b/src/StarBlog.Api/Apis/Admin/ConfigController.cs @@ -4,6 +4,7 @@ using StarBlog.Data.Models; using StarBlog.Api.Extensions; using StarBlog.Application.Services; +using StarBlog.Application.ViewModels.Config; namespace StarBlog.Api.Apis.Admin; @@ -61,4 +62,4 @@ public ApiResponse Delete(string key) { var item = _service.GetByKey(key); return item == null ? ApiResponse.NotFound() : ApiResponse.Ok($"已删除 {_service.DeleteByKey(key)} 条数据"); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Api/Apis/Admin/VisitRecordController.cs b/src/StarBlog.Api/Apis/Admin/VisitRecordController.cs index eaca4d4..b84a370 100644 --- a/src/StarBlog.Api/Apis/Admin/VisitRecordController.cs +++ b/src/StarBlog.Api/Apis/Admin/VisitRecordController.cs @@ -56,6 +56,7 @@ public async Task> Overview([FromQuery] VisitRecordPa /// /// 趋势数据 /// + /// 访问记录筛选条件 /// 查看最近几天的数据,默认7天 /// [HttpGet("[action]")] @@ -74,6 +75,7 @@ public async Task>> HourlyTrend([FromQuery] VisitR /// /// 获取地理信息筛选参数 /// + /// 访问记录筛选条件 /// 可选 country, province, city [HttpGet("[action]")] public async Task>> GeoFilterParams([FromQuery] VisitRecordParameters p, string param = "country") { diff --git a/src/StarBlog.Api/StarBlog.Api.csproj b/src/StarBlog.Api/StarBlog.Api.csproj index bac9fc6..30c8ff9 100644 --- a/src/StarBlog.Api/StarBlog.Api.csproj +++ b/src/StarBlog.Api/StarBlog.Api.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/StarBlog.Application/Contrib/SiteMessage/Message.cs b/src/StarBlog.Application/Contrib/SiteMessage/Message.cs index 80b9688..2636175 100644 --- a/src/StarBlog.Application/Contrib/SiteMessage/Message.cs +++ b/src/StarBlog.Application/Contrib/SiteMessage/Message.cs @@ -1,7 +1,7 @@ namespace StarBlog.Application.Contrib.SiteMessage; public class Message { - public string Tag { get; set; } + public string Tag { get; set; } = string.Empty; public string? Title { get; set; } public string? Content { get; set; } } @@ -12,4 +12,4 @@ public static class MessageTags { public const string Success = "success"; public const string Warning = "warning"; public const string Error = "danger"; -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/Models/Config/Auth.cs b/src/StarBlog.Application/Models/Config/Auth.cs index e60dc39..a5de718 100644 --- a/src/StarBlog.Application/Models/Config/Auth.cs +++ b/src/StarBlog.Application/Models/Config/Auth.cs @@ -1,11 +1,11 @@ namespace StarBlog.Application.Models.Config; public class Auth { - public Jwt Jwt { get; set; } + public Jwt Jwt { get; set; } = new(); } public class Jwt { - public string Issuer { get; set; } - public string Audience { get; set; } - public string Key { get; set; } -} \ No newline at end of file + public string Issuer { get; set; } = string.Empty; + public string Audience { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Application/Services/CommentService.cs b/src/StarBlog.Application/Services/CommentService.cs index 1e21bad..9ef541b 100644 --- a/src/StarBlog.Application/Services/CommentService.cs +++ b/src/StarBlog.Application/Services/CommentService.cs @@ -89,7 +89,7 @@ public CommentService(ILogger logger, IBaseRepository c var data = await querySet.Page(param.Page, param.PageSize) .Include(a => a.AnonymousUser) - .Include(a => a.Parent.AnonymousUser) + .Include(a => a.Parent!.AnonymousUser) .ToListAsync(); IPagedList pagedList = new StaticPagedList(data, param.Page, param.PageSize, Convert.ToInt32(await querySet.CountAsync())); @@ -171,6 +171,8 @@ public static bool IsValidEmail(string email) { /// /// 验证一次性密码 /// + /// 邮箱地址 + /// 一次性验证码 /// 验证通过后是否清除 public bool VerifyOtp(string email, string otp, bool clear = true) { var cacheKey = $"comment-otp-{email}"; diff --git a/src/StarBlog.Application/Services/ConfigService.cs b/src/StarBlog.Application/Services/ConfigService.cs index 5011f95..9311326 100644 --- a/src/StarBlog.Application/Services/ConfigService.cs +++ b/src/StarBlog.Application/Services/ConfigService.cs @@ -26,7 +26,7 @@ public List GetAll() { // 尝试读取初始化配置 var section = _conf.GetSection($"StarBlog:Initial:{key}"); if (!section.Exists()) return null; - item = new ConfigItem { Key = key, Value = section.Value, Description = "Initial" }; + item = new ConfigItem { Key = key, Value = section.Value ?? string.Empty, Description = "Initial" }; item = AddOrUpdate(item); } @@ -65,4 +65,4 @@ public string this[string key] { AddOrUpdate(item); } } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/Services/EmailService.cs b/src/StarBlog.Application/Services/EmailService.cs index 22b079c..97acb1d 100644 --- a/src/StarBlog.Application/Services/EmailService.cs +++ b/src/StarBlog.Application/Services/EmailService.cs @@ -24,9 +24,10 @@ public async Task SendEmailAsync(string subject, string bo /// /// 发送邮箱验证码 - /// 生成随机验证码 - /// 只生成验证码,不发邮件 /// + /// 收件邮箱 + /// 只生成验证码,不发邮件 + /// 生成随机验证码 public async Task SendOtpMail(string email, bool mock = false) { var otp = Random.Shared.NextInt64(1000, 9999).ToString(); diff --git a/src/StarBlog.Application/Services/PhotoService.cs b/src/StarBlog.Application/Services/PhotoService.cs index 5c2fecf..1b84867 100644 --- a/src/StarBlog.Application/Services/PhotoService.cs +++ b/src/StarBlog.Application/Services/PhotoService.cs @@ -76,6 +76,7 @@ public async Task> GetFeaturedPhotos() { /// 质量,默认85 public async Task GetThumb(string id, int width = 0, int quality = 85) { var photo = await _photoRepo.Where(a => a.Id == id).FirstAsync(); + if (photo == null) throw new InvalidOperationException($"图片 {id} 不存在。"); using var image = await Image.LoadAsync(GetPhotoFilePath(photo)); if (width != 0) { @@ -91,7 +92,9 @@ public async Task GetThumb(string id, int width = 0, int quality = 85) { } public async Task Update(PhotoUpdateDto dto) { + if (string.IsNullOrWhiteSpace(dto.Id)) throw new ArgumentException("图片 ID 不能为空。", nameof(dto)); var photo = await GetById(dto.Id); + if (photo == null) throw new InvalidOperationException($"图片 {dto.Id} 不存在。"); photo = _mapper.Map(dto, photo); await _photoRepo.UpdateAsync(photo); return photo; @@ -241,6 +244,7 @@ private string GetPhotoFilePath(Photo photo) { private async Task BuildPhotoData(Photo photo) { var savePath = GetPhotoFilePath(photo); var imgInfo = await Image.IdentifyAsync(savePath); + if (imgInfo == null) throw new InvalidOperationException($"无法识别图片文件:{savePath}"); photo.Width = imgInfo.Width; photo.Height = imgInfo.Height; diff --git a/src/StarBlog.Application/Services/ThemeService.cs b/src/StarBlog.Application/Services/ThemeService.cs index 9444a3e..46bb1e9 100644 --- a/src/StarBlog.Application/Services/ThemeService.cs +++ b/src/StarBlog.Application/Services/ThemeService.cs @@ -28,7 +28,7 @@ public ThemeService(IAppPathProvider paths) { } public class Theme { - public string Name { get; set; } - public string Path { get; set; } - public string CssUrl { get; set; } + public string Name { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string CssUrl { get; set; } = string.Empty; } diff --git a/src/StarBlog.Application/Services/VisitRecordServices/VisitRecordService.cs b/src/StarBlog.Application/Services/VisitRecordServices/VisitRecordService.cs index 652c36d..28b24a2 100644 --- a/src/StarBlog.Application/Services/VisitRecordServices/VisitRecordService.cs +++ b/src/StarBlog.Application/Services/VisitRecordServices/VisitRecordService.cs @@ -269,12 +269,8 @@ public async Task> GetReferrerDomains(VisitRecordParameters // 在内存中进行 Uri 解析和分组 return records .Select(r => { - try { - return new Uri(r!).Host; - } - catch { - return r; - } // 处理无效的 URL + if (string.IsNullOrWhiteSpace(r)) return string.Empty; + return Uri.TryCreate(r, UriKind.Absolute, out var uri) ? uri.Host : r; }) .GroupBy(host => host) .Select(g => new ReferrerDomain { Domain = g.Key, Count = g.Count() }) @@ -403,9 +399,9 @@ public async Task GetTechDistribution(VisitRecordParameters p) .ToListAsync(); return new TechDistribution { - Browsers = browserStats.Select(b => new NameCountPair { Name = b.Browser, Count = b.Count }).ToList(), - OperatingSystems = osStats.Select(o => new NameCountPair { Name = o.OS, Count = o.Count }).ToList(), - Devices = deviceStats.Select(d => new NameCountPair { Name = d.Device, Count = d.Count }).ToList() + Browsers = browserStats.Select(b => new NameCountPair { Name = b.Browser ?? string.Empty, Count = b.Count }).ToList(), + OperatingSystems = osStats.Select(o => new NameCountPair { Name = o.OS ?? string.Empty, Count = o.Count }).ToList(), + Devices = deviceStats.Select(d => new NameCountPair { Name = d.Device ?? string.Empty, Count = d.Count }).ToList() }; } diff --git a/src/StarBlog.Application/ViewModels/Auth/LoginToken.cs b/src/StarBlog.Application/ViewModels/Auth/LoginToken.cs index 85db940..e5fea22 100644 --- a/src/StarBlog.Application/ViewModels/Auth/LoginToken.cs +++ b/src/StarBlog.Application/ViewModels/Auth/LoginToken.cs @@ -1,6 +1,6 @@ namespace StarBlog.Application.ViewModels.Auth; public class LoginToken { - public string Token { get; set; } + public string Token { get; set; } = string.Empty; public DateTime Expiration { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Auth/LoginUser.cs b/src/StarBlog.Application/ViewModels/Auth/LoginUser.cs index 6e1b055..ff29dee 100644 --- a/src/StarBlog.Application/ViewModels/Auth/LoginUser.cs +++ b/src/StarBlog.Application/ViewModels/Auth/LoginUser.cs @@ -1,6 +1,6 @@ namespace StarBlog.Application.ViewModels.Auth; public class LoginUser { - public string Username { get; set; } - public string Password { get; set; } -} \ No newline at end of file + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Application/ViewModels/Blog/PostUpdateDto.cs b/src/StarBlog.Application/ViewModels/Blog/PostUpdateDto.cs index 586275d..e521aa1 100644 --- a/src/StarBlog.Application/ViewModels/Blog/PostUpdateDto.cs +++ b/src/StarBlog.Application/ViewModels/Blog/PostUpdateDto.cs @@ -1,12 +1,12 @@ namespace StarBlog.Application.ViewModels.Blog; public class PostUpdateDto { - public string Id { get; set; } + public string Id { get; set; } = string.Empty; /// /// 标题 /// - public string Title { get; set; } + public string Title { get; set; } = string.Empty; /// /// 文章链接,设置后可以通过以下形式访问文章 @@ -27,15 +27,15 @@ public class PostUpdateDto { /// /// 梗概 /// - public string Summary { get; set; } + public string Summary { get; set; } = string.Empty; /// /// 内容(markdown格式) /// - public string Content { get; set; } + public string Content { get; set; } = string.Empty; /// /// 分类ID /// public int CategoryId { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Categories/CategoryCreationDto.cs b/src/StarBlog.Application/ViewModels/Categories/CategoryCreationDto.cs index f65854e..2fc4a7a 100644 --- a/src/StarBlog.Application/ViewModels/Categories/CategoryCreationDto.cs +++ b/src/StarBlog.Application/ViewModels/Categories/CategoryCreationDto.cs @@ -1,7 +1,7 @@ namespace StarBlog.Application.ViewModels.Categories; public record CategoryCreationDto { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public int ParentId { get; set; } public bool Visible { get; set; } = true; -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Categories/CategoryNode.cs b/src/StarBlog.Application/ViewModels/Categories/CategoryNode.cs index 7817861..260b3e7 100644 --- a/src/StarBlog.Application/ViewModels/Categories/CategoryNode.cs +++ b/src/StarBlog.Application/ViewModels/Categories/CategoryNode.cs @@ -4,6 +4,6 @@ public class CategoryNode { public int Id { get; set; } public string? text { get; set; } public string? href { get; set; } - public List tags { get; set; } + public List tags { get; set; } = []; public List? nodes { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto.cs b/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto.cs index dabdc4c..56f6ed6 100644 --- a/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto.cs +++ b/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto.cs @@ -4,12 +4,12 @@ public class FeaturedCategoryCreationDto { /// /// 重新定义的推荐名称 /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// 推荐分类解释 /// - public string Description { get; set; } + public string Description { get; set; } = string.Empty; /// /// 图标 @@ -20,5 +20,5 @@ public class FeaturedCategoryCreationDto { /// fa-brands fa-android /// /// - public string IconCssClass { get; set; } -} \ No newline at end of file + public string IconCssClass { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto2.cs b/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto2.cs index 22a8e9f..bee2b36 100644 --- a/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto2.cs +++ b/src/StarBlog.Application/ViewModels/Categories/FeaturedCategoryCreationDto2.cs @@ -4,12 +4,12 @@ public class FeaturedCategoryCreationDto2 { /// /// 重新定义的推荐名称 /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// 推荐分类解释 /// - public string Description { get; set; } + public string Description { get; set; } = string.Empty; /// /// 图标 @@ -20,10 +20,10 @@ public class FeaturedCategoryCreationDto2 { /// fa-brands fa-android /// /// - public string IconCssClass { get; set; } + public string IconCssClass { get; set; } = string.Empty; /// /// 分类ID /// public int CategoryId { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Comments/CommentCreationDto.cs b/src/StarBlog.Application/ViewModels/Comments/CommentCreationDto.cs index 62fc9d1..c51ffd1 100644 --- a/src/StarBlog.Application/ViewModels/Comments/CommentCreationDto.cs +++ b/src/StarBlog.Application/ViewModels/Comments/CommentCreationDto.cs @@ -6,24 +6,24 @@ public class CommentCreationDto { public string? ParentId { get; set; } [Required] - public string PostId { get; set; } + public string PostId { get; set; } = string.Empty; [MinLength(2, ErrorMessage = "长度在 2 到 20 个字符")] [MaxLength(20, ErrorMessage = "长度在 2 到 20 个字符")] - public string UserName { get; set; } + public string UserName { get; set; } = string.Empty; [Required(ErrorMessage = "邮箱地址不能为空")] [EmailAddress(ErrorMessage = "邮箱格式不正确")] - public string Email { get; set; } + public string Email { get; set; } = string.Empty; [Url(ErrorMessage = "请输入正确的url")] public string? Url { get; set; } [Required(ErrorMessage = "邮箱验证码不能为空")] [StringLength(4, ErrorMessage = "长度为 4 个字符")] - public string EmailOtp { get; set; } + public string EmailOtp { get; set; } = string.Empty; [Required(ErrorMessage = "评论内容不能为空")] [MaxLength(300, ErrorMessage = "评论最大长度为300个字符")] - public string Content { get; set; } -} \ No newline at end of file + public string Content { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Application/ViewModels/Comments/CommentRejectDto.cs b/src/StarBlog.Application/ViewModels/Comments/CommentRejectDto.cs index 145edee..e052777 100644 --- a/src/StarBlog.Application/ViewModels/Comments/CommentRejectDto.cs +++ b/src/StarBlog.Application/ViewModels/Comments/CommentRejectDto.cs @@ -1,5 +1,5 @@ namespace StarBlog.Application.ViewModels.Comments; public class CommentRejectDto { - public string Reason { get; set; } -} \ No newline at end of file + public string Reason { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Application/ViewModels/Config/ConfigItemCreationDto.cs b/src/StarBlog.Application/ViewModels/Config/ConfigItemCreationDto.cs index 6075134..7f7fd58 100644 --- a/src/StarBlog.Application/ViewModels/Config/ConfigItemCreationDto.cs +++ b/src/StarBlog.Application/ViewModels/Config/ConfigItemCreationDto.cs @@ -1,5 +1,7 @@ -public class ConfigItemCreationDto { - public string Key { get; set; } - public string Value { get; set; } +namespace StarBlog.Application.ViewModels.Config; + +public class ConfigItemCreationDto { + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; public string? Description { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Config/ConfigItemUpdateDto.cs b/src/StarBlog.Application/ViewModels/Config/ConfigItemUpdateDto.cs index 9209f75..bb8e1ce 100644 --- a/src/StarBlog.Application/ViewModels/Config/ConfigItemUpdateDto.cs +++ b/src/StarBlog.Application/ViewModels/Config/ConfigItemUpdateDto.cs @@ -1,4 +1,6 @@ -public class ConfigItemUpdateDto { - public string Value { get; set; } +namespace StarBlog.Application.ViewModels.Config; + +public class ConfigItemUpdateDto { + public string Value { get; set; } = string.Empty; public string? Description { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Links/LinkCreationDto.cs b/src/StarBlog.Application/ViewModels/Links/LinkCreationDto.cs index 6d98895..4d7da40 100644 --- a/src/StarBlog.Application/ViewModels/Links/LinkCreationDto.cs +++ b/src/StarBlog.Application/ViewModels/Links/LinkCreationDto.cs @@ -4,20 +4,20 @@ public class LinkCreationDto { /// /// 网站名称 /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// 介绍 /// - public string Description { get; set; } + public string Description { get; set; } = string.Empty; /// /// 网址 /// - public string Url { get; set; } + public string Url { get; set; } = string.Empty; /// /// 是否显示 /// public bool Visible { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/Photography/PhotoCreationDto.cs b/src/StarBlog.Application/ViewModels/Photography/PhotoCreationDto.cs index 346442c..66ce1f6 100644 --- a/src/StarBlog.Application/ViewModels/Photography/PhotoCreationDto.cs +++ b/src/StarBlog.Application/ViewModels/Photography/PhotoCreationDto.cs @@ -7,11 +7,11 @@ public class PhotoCreationDto { /// 作品标题 /// [Required(ErrorMessage = "作品标题不能为空")] - public string Title { get; set; } + public string Title { get; set; } = string.Empty; /// /// 拍摄地点 /// [Required(ErrorMessage = "拍摄地点不能为空")] - public string Location { get; set; } -} \ No newline at end of file + public string Location { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Application/ViewModels/Photography/PhotoUpdateDto.cs b/src/StarBlog.Application/ViewModels/Photography/PhotoUpdateDto.cs index 8706fa1..a33551a 100644 --- a/src/StarBlog.Application/ViewModels/Photography/PhotoUpdateDto.cs +++ b/src/StarBlog.Application/ViewModels/Photography/PhotoUpdateDto.cs @@ -9,13 +9,13 @@ public class PhotoUpdateDto { /// 作品标题 /// [Required(ErrorMessage = "作品标题不能为空")] - public string Title { get; set; } + public string Title { get; set; } = string.Empty; /// /// 拍摄地点 /// [Required(ErrorMessage = "拍摄地点不能为空")] - public string Location { get; set; } + public string Location { get; set; } = string.Empty; public DateTime CreateTime { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/ConversionStats.cs b/src/StarBlog.Application/ViewModels/VisitRecord/ConversionStats.cs index d499246..86663b2 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/ConversionStats.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/ConversionStats.cs @@ -1,9 +1,9 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class ConversionStats { - public string TargetPath { get; set; } + public string TargetPath { get; set; } = string.Empty; public int TotalSessions { get; set; } public int ConvertedSessions { get; set; } public double ConversionRate { get; set; } - public List TopEntryPages { get; set; } -} \ No newline at end of file + public List TopEntryPages { get; set; } = []; +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/DailyTrend.cs b/src/StarBlog.Application/ViewModels/VisitRecord/DailyTrend.cs index cdb4703..cf9463b 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/DailyTrend.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/DailyTrend.cs @@ -2,10 +2,10 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class DailyTrend { public DateTime Time { get; set; } - public string Date { get; set; } + public string Date { get; set; } = string.Empty; public int Total { get; set; } public int Pv { get; set; } public int Uv { get; set; } public long Api { get; set; } public long Spider { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/ReferrerDomain.cs b/src/StarBlog.Application/ViewModels/VisitRecord/ReferrerDomain.cs index 5f1b48c..15a9251 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/ReferrerDomain.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/ReferrerDomain.cs @@ -1,6 +1,6 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class ReferrerDomain { - public string Domain { get; set; } + public string Domain { get; set; } = string.Empty; public int Count { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/SlowRequest.cs b/src/StarBlog.Application/ViewModels/VisitRecord/SlowRequest.cs index 7163f68..c8552b8 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/SlowRequest.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/SlowRequest.cs @@ -1,9 +1,9 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class SlowRequest { - public string Path { get; set; } - public string Method { get; set; } + public string Path { get; set; } = string.Empty; + public string Method { get; set; } = string.Empty; public long ResponseTimeMs { get; set; } public DateTime Time { get; set; } public int StatusCode { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/TechDistribution.cs b/src/StarBlog.Application/ViewModels/VisitRecord/TechDistribution.cs index 6e7b64a..d3c2129 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/TechDistribution.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/TechDistribution.cs @@ -1,12 +1,12 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class TechDistribution { - public List Browsers { get; set; } - public List OperatingSystems { get; set; } - public List Devices { get; set; } + public List Browsers { get; set; } = []; + public List OperatingSystems { get; set; } = []; + public List Devices { get; set; } = []; } public class NameCountPair { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public int Count { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/TopPath.cs b/src/StarBlog.Application/ViewModels/VisitRecord/TopPath.cs index 6763a45..5a894b3 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/TopPath.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/TopPath.cs @@ -1,6 +1,6 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class TopPath { - public string Path { get; set; } + public string Path { get; set; } = string.Empty; public int Count { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Application/ViewModels/VisitRecord/UserAgentFilterParams.cs b/src/StarBlog.Application/ViewModels/VisitRecord/UserAgentFilterParams.cs index c9bbe01..297869b 100644 --- a/src/StarBlog.Application/ViewModels/VisitRecord/UserAgentFilterParams.cs +++ b/src/StarBlog.Application/ViewModels/VisitRecord/UserAgentFilterParams.cs @@ -1,7 +1,7 @@ namespace StarBlog.Application.ViewModels.VisitRecord; public class UserAgentFilterParams { - public List OS { get; set; } - public List Device { get; set; } - public List UserAgent { get; set; } -} \ No newline at end of file + public List OS { get; set; } = []; + public List Device { get; set; } = []; + public List UserAgent { get; set; } = []; +} diff --git a/src/StarBlog.Content/Extensions/StringExt.cs b/src/StarBlog.Content/Extensions/StringExt.cs index d292673..dfe352d 100644 --- a/src/StarBlog.Content/Extensions/StringExt.cs +++ b/src/StarBlog.Content/Extensions/StringExt.cs @@ -3,14 +3,16 @@ namespace StarBlog.Content.Extensions; public static class StringExt { - public static string Limit(this string str, int length) { + public static string Limit(this string? str, int length) { + str ??= string.Empty; return str.Length <= length ? str : str[..length]; } /// /// 限制字符串显示长度并在末尾添加省略号 /// - public static string LimitWithEllipsis(this string str, int length) { + public static string LimitWithEllipsis(this string? str, int length) { + str ??= string.Empty; return str.Length <= length ? str : $"{str[..length]}..."; } diff --git a/src/StarBlog.Content/StarBlog.Content.csproj b/src/StarBlog.Content/StarBlog.Content.csproj index 157b6a4..d40f7c1 100644 --- a/src/StarBlog.Content/StarBlog.Content.csproj +++ b/src/StarBlog.Content/StarBlog.Content.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/StarBlog.Data/Extensions/ConfigureFreeSql.cs b/src/StarBlog.Data/Extensions/ConfigureFreeSql.cs index c41052f..e7b80c7 100644 --- a/src/StarBlog.Data/Extensions/ConfigureFreeSql.cs +++ b/src/StarBlog.Data/Extensions/ConfigureFreeSql.cs @@ -1,4 +1,4 @@ -using FreeSql; +using FreeSql; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -6,7 +6,9 @@ namespace StarBlog.Data.Extensions; public static class ConfigureFreeSql { public static void AddFreeSql(this IServiceCollection services, IConfiguration configuration) { - var freeSql = FreeSqlFactory.Create(configuration.GetConnectionString("SQLite")); + var connectionString = configuration.GetConnectionString("SQLite") + ?? throw new InvalidOperationException("缺少 SQLite 连接字符串配置。"); + var freeSql = FreeSqlFactory.Create(connectionString); // var freeSql = FreeSqlFactory.CreateMySql(configuration.GetConnectionString("MySql")); // var freeSql = FreeSqlFactory.CreatePostgresSql(configuration.GetConnectionString("PostgresSql")); @@ -15,4 +17,4 @@ public static void AddFreeSql(this IServiceCollection services, IConfiguration c // 仓储模式支持 services.AddFreeRepository(); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Data/StarBlog.Data.csproj b/src/StarBlog.Data/StarBlog.Data.csproj index 3dd7c34..ea63b11 100644 --- a/src/StarBlog.Data/StarBlog.Data.csproj +++ b/src/StarBlog.Data/StarBlog.Data.csproj @@ -22,6 +22,7 @@ + diff --git a/src/StarBlog.Web/.npmrc b/src/StarBlog.Web/.npmrc index e69de29..97b895e 100644 --- a/src/StarBlog.Web/.npmrc +++ b/src/StarBlog.Web/.npmrc @@ -0,0 +1 @@ +ignore-scripts=true diff --git a/src/StarBlog.Web/Apis/Admin/ConfigController.cs b/src/StarBlog.Web/Apis/Admin/ConfigController.cs index 0f25e23..ecf8179 100644 --- a/src/StarBlog.Web/Apis/Admin/ConfigController.cs +++ b/src/StarBlog.Web/Apis/Admin/ConfigController.cs @@ -1,9 +1,10 @@ -using CodeLab.Share.ViewModels.Response; +using CodeLab.Share.ViewModels.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using StarBlog.Data.Models; using StarBlog.Web.Extensions; using StarBlog.Web.Services; +using StarBlog.Web.ViewModels.Config; namespace StarBlog.Web.Apis.Admin; @@ -61,4 +62,4 @@ public ApiResponse Delete(string key) { var item = _service.GetByKey(key); return item == null ? ApiResponse.NotFound() : ApiResponse.Ok($"已删除 {_service.DeleteByKey(key)} 条数据"); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Apis/Admin/VisitRecordController.cs b/src/StarBlog.Web/Apis/Admin/VisitRecordController.cs index d0b461d..44f025b 100644 --- a/src/StarBlog.Web/Apis/Admin/VisitRecordController.cs +++ b/src/StarBlog.Web/Apis/Admin/VisitRecordController.cs @@ -56,6 +56,7 @@ public async Task> Overview([FromQuery] VisitRecordPa /// /// 趋势数据 /// + /// 访问记录筛选条件 /// 查看最近几天的数据,默认7天 /// [HttpGet("[action]")] @@ -74,6 +75,7 @@ public async Task>> HourlyTrend([FromQuery] VisitR /// /// 获取地理信息筛选参数 /// + /// 访问记录筛选条件 /// 可选 country, province, city [HttpGet("[action]")] public async Task>> GeoFilterParams([FromQuery] VisitRecordParameters p, string param = "country") { diff --git a/src/StarBlog.Web/Controllers/BlogController.cs b/src/StarBlog.Web/Controllers/BlogController.cs index dcce5d0..2ab5f4f 100644 --- a/src/StarBlog.Web/Controllers/BlogController.cs +++ b/src/StarBlog.Web/Controllers/BlogController.cs @@ -1,4 +1,4 @@ -using FreeSql; +using FreeSql; using Microsoft.AspNetCore.Mvc; using StarBlog.Data.Models; using StarBlog.Web.Contrib.SiteMessage; @@ -117,9 +117,9 @@ public async Task Post(string id, [FromQuery] string? lang) { if (!string.IsNullOrEmpty(lang) && lang != "zh") { var translation = translations.FirstOrDefault(t => t.Language == lang); if (translation != null) { - postViewModel.Title = translation.Title; - postViewModel.Summary = translation.Summary; - postViewModel.Content = translation.Content; + postViewModel.Title = translation.Title ?? postViewModel.Title; + postViewModel.Summary = translation.Summary ?? postViewModel.Summary; + postViewModel.Content = translation.Content ?? postViewModel.Content; postViewModel.CurrentLanguage = lang; if (!string.IsNullOrEmpty(translation.Content)) { postViewModel.ContentHtml = PostService.GetContentHtml(translation); @@ -174,4 +174,4 @@ public IActionResult RandomPost() { public IActionResult Temp() { return View(); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Controllers/RssController.cs b/src/StarBlog.Web/Controllers/RssController.cs index 610166e..e3d7b3a 100644 --- a/src/StarBlog.Web/Controllers/RssController.cs +++ b/src/StarBlog.Web/Controllers/RssController.cs @@ -44,9 +44,10 @@ public async Task Feed() { var items = new List(); foreach (var item in posts) { var postUrl = Url.Action("Post", "Blog", new {id = item.Id}, HttpContext.Request.Scheme); + var rssItemUrl = postUrl ?? _conf["host"]; items.Add(new SyndicationItem(item.Title, new TextSyndicationContent(PostService.GetContentHtml(item), TextSyndicationContentKind.Html), - new Uri(postUrl), item.Id, item.LastUpdateTime + new Uri(rssItemUrl), item.Id, item.LastUpdateTime ) { Categories = {new SyndicationCategory(item.Category?.Name)}, Authors = {new SyndicationPerson("admin@deali.cn", "DealiAxy", "https://deali.cn")}, @@ -72,4 +73,4 @@ public async Task Feed() { return File(stream.ToArray(), "application/xml; charset=utf-8"); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Controllers/SearchController.cs b/src/StarBlog.Web/Controllers/SearchController.cs index f05cc30..5943cf7 100644 --- a/src/StarBlog.Web/Controllers/SearchController.cs +++ b/src/StarBlog.Web/Controllers/SearchController.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FreeSql; using Microsoft.AspNetCore.Mvc; using StarBlog.Data.Models; @@ -20,14 +20,14 @@ public IActionResult Blog(string keyword, int categoryId = 0, int page = 1, int .Where(a => a.IsPublish) .Where(a => a.Title!.Contains(keyword) || - a.Content.Contains(keyword) + a.Content!.Contains(keyword) ) .Include(a => a.Category) .ToList() .Select(p => new SearchPost { Post = p, - TitleScore = p.Title.ToLower().Split(keyword).Length - 1, - ContentScore = p.Content?.ToLower().Split(keyword).Length - 1 ?? 0, + TitleScore = (p.Title ?? string.Empty).ToLower().Split(keyword).Length - 1, + ContentScore = (p.Content ?? string.Empty).ToLower().Split(keyword).Length - 1, }) .OrderByDescending(x => x.Score) .ToList(); @@ -35,7 +35,7 @@ public IActionResult Blog(string keyword, int categoryId = 0, int page = 1, int // 实现搜索结果高亮 var regex = new Regex(Regex.Escape(keyword), RegexOptions.IgnoreCase); foreach (var item in searchPosts) { - item.HighlightedTitle = regex.Replace(item.Post.Title, m => $"{m.Value}"); + item.HighlightedTitle = regex.Replace(item.Post.Title ?? string.Empty, m => $"{m.Value}"); item.HighlightedSnippet = GetHighlightedSnippet(item.Post.Content, keyword); } @@ -45,7 +45,7 @@ public IActionResult Blog(string keyword, int categoryId = 0, int page = 1, int }); } - public static string GetHighlightedSnippet(string content, string keyword, int snippetLength = 100) { + public static string GetHighlightedSnippet(string? content, string keyword, int snippetLength = 100) { if (string.IsNullOrEmpty(content) || string.IsNullOrEmpty(keyword)) return string.Empty; @@ -74,4 +74,4 @@ public static string GetHighlightedSnippet(string content, string keyword, int s return snippet; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Extensions/ConfigureAuth.cs b/src/StarBlog.Web/Extensions/ConfigureAuth.cs index a60fbde..970a7ba 100644 --- a/src/StarBlog.Web/Extensions/ConfigureAuth.cs +++ b/src/StarBlog.Web/Extensions/ConfigureAuth.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using StarBlog.Web.Models.Config; @@ -14,7 +14,7 @@ public static void AddAuth(this IServiceCollection services, IConfiguration conf options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { - var authSetting = configuration.GetSection(nameof(Auth)).Get(); + var authSetting = configuration.GetSection(nameof(Auth)).Get() ?? new Auth(); options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateLifetime = true, @@ -27,4 +27,4 @@ public static void AddAuth(this IServiceCollection services, IConfiguration conf }; }); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Extensions/HttpRequestExt.cs b/src/StarBlog.Web/Extensions/HttpRequestExt.cs index e24b33e..13e5a3e 100644 --- a/src/StarBlog.Web/Extensions/HttpRequestExt.cs +++ b/src/StarBlog.Web/Extensions/HttpRequestExt.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace StarBlog.Web.Extensions; @@ -22,10 +22,11 @@ public static class HttpRequestExt { public static bool IsMobileBrowser(this HttpRequest request) { var userAgent = request.GetUserAgent(); - return b.IsMatch(userAgent) || v.IsMatch(userAgent[..4]); + var prefix = userAgent.Length >= 4 ? userAgent[..4] : userAgent; + return b.IsMatch(userAgent) || v.IsMatch(prefix); } public static string GetUserAgent(this HttpRequest request) { - return request.Headers["User-Agent"]; + return request.Headers["User-Agent"].ToString(); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Models/Config/Auth.cs b/src/StarBlog.Web/Models/Config/Auth.cs index 9cba26b..b1f5110 100644 --- a/src/StarBlog.Web/Models/Config/Auth.cs +++ b/src/StarBlog.Web/Models/Config/Auth.cs @@ -1,11 +1,11 @@ -namespace StarBlog.Web.Models.Config; +namespace StarBlog.Web.Models.Config; public class Auth { - public Jwt Jwt { get; set; } + public Jwt Jwt { get; set; } = new(); } public class Jwt { - public string Issuer { get; set; } - public string Audience { get; set; } - public string Key { get; set; } -} \ No newline at end of file + public string Issuer { get; set; } = string.Empty; + public string Audience { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/Properties/launchSettings.json b/src/StarBlog.Web/Properties/launchSettings.json index 98ba5c9..709fe9c 100644 --- a/src/StarBlog.Web/Properties/launchSettings.json +++ b/src/StarBlog.Web/Properties/launchSettings.json @@ -11,7 +11,7 @@ "StarBlog.Web": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, "applicationUrl": "http://localhost:5038", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/StarBlog.Web/Services/CommentService.cs b/src/StarBlog.Web/Services/CommentService.cs index 72fde98..f58f3ba 100644 --- a/src/StarBlog.Web/Services/CommentService.cs +++ b/src/StarBlog.Web/Services/CommentService.cs @@ -89,7 +89,7 @@ public CommentService(ILogger logger, IBaseRepository c var data = await querySet.Page(param.Page, param.PageSize) .Include(a => a.AnonymousUser) - .Include(a => a.Parent.AnonymousUser) + .Include(a => a.Parent!.AnonymousUser) .ToListAsync(); IPagedList pagedList = new StaticPagedList(data, param.Page, param.PageSize, Convert.ToInt32(await querySet.CountAsync())); @@ -171,6 +171,8 @@ public static bool IsValidEmail(string email) { /// /// 验证一次性密码 /// + /// 邮箱地址 + /// 一次性验证码 /// 验证通过后是否清除 public bool VerifyOtp(string email, string otp, bool clear = true) { var cacheKey = $"comment-otp-{email}"; diff --git a/src/StarBlog.Web/Services/ConfigService.cs b/src/StarBlog.Web/Services/ConfigService.cs index 3f75baa..d40a942 100644 --- a/src/StarBlog.Web/Services/ConfigService.cs +++ b/src/StarBlog.Web/Services/ConfigService.cs @@ -1,4 +1,4 @@ -using FreeSql; +using FreeSql; using StarBlog.Data.Models; namespace StarBlog.Web.Services; @@ -26,7 +26,7 @@ public List GetAll() { // 尝试读取初始化配置 var section = _conf.GetSection($"StarBlog:Initial:{key}"); if (!section.Exists()) return null; - item = new ConfigItem { Key = key, Value = section.Value, Description = "Initial" }; + item = new ConfigItem { Key = key, Value = section.Value ?? string.Empty, Description = "Initial" }; item = AddOrUpdate(item); } @@ -65,4 +65,4 @@ public string this[string key] { AddOrUpdate(item); } } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Services/EmailService.cs b/src/StarBlog.Web/Services/EmailService.cs index 5eff537..78f2297 100644 --- a/src/StarBlog.Web/Services/EmailService.cs +++ b/src/StarBlog.Web/Services/EmailService.cs @@ -24,9 +24,10 @@ public async Task SendEmailAsync(string subject, string bo /// /// 发送邮箱验证码 - /// 生成随机验证码 - /// 只生成验证码,不发邮件 /// + /// 收件邮箱 + /// 只生成验证码,不发邮件 + /// 生成随机验证码 public async Task SendOtpMail(string email, bool mock = false) { var otp = Random.Shared.NextInt64(1000, 9999).ToString(); diff --git a/src/StarBlog.Web/Services/PhotoService.cs b/src/StarBlog.Web/Services/PhotoService.cs index 2cdbeca..6a8fc54 100644 --- a/src/StarBlog.Web/Services/PhotoService.cs +++ b/src/StarBlog.Web/Services/PhotoService.cs @@ -73,6 +73,7 @@ public async Task> GetFeaturedPhotos() { /// 质量,默认85 public async Task GetThumb(string id, int width = 0, int quality = 85) { var photo = await _photoRepo.Where(a => a.Id == id).FirstAsync(); + if (photo == null) throw new InvalidOperationException($"图片 {id} 不存在。"); using var image = await Image.LoadAsync(GetPhotoFilePath(photo)); if (width != 0) { @@ -88,7 +89,9 @@ public async Task GetThumb(string id, int width = 0, int quality = 85) { } public async Task Update(PhotoUpdateDto dto) { + if (string.IsNullOrWhiteSpace(dto.Id)) throw new ArgumentException("图片 ID 不能为空。", nameof(dto)); var photo = await GetById(dto.Id); + if (photo == null) throw new InvalidOperationException($"图片 {dto.Id} 不存在。"); photo = _mapper.Map(dto, photo); await _photoRepo.UpdateAsync(photo); return photo; @@ -236,6 +239,7 @@ private string GetPhotoFilePath(Photo photo) { private async Task BuildPhotoData(Photo photo) { var savePath = GetPhotoFilePath(photo); var imgInfo = await Image.IdentifyAsync(savePath); + if (imgInfo == null) throw new InvalidOperationException($"无法识别图片文件:{savePath}"); photo.Width = imgInfo.Width; photo.Height = imgInfo.Height; diff --git a/src/StarBlog.Web/Services/PostService.cs b/src/StarBlog.Web/Services/PostService.cs index 8cf211f..440500d 100644 --- a/src/StarBlog.Web/Services/PostService.cs +++ b/src/StarBlog.Web/Services/PostService.cs @@ -225,7 +225,7 @@ public async Task GetPostViewModel(Post post, bool md2Html = true ), CreationTime = post.CreationTime, LastUpdateTime = post.LastUpdateTime, - Category = post.Category, + Category = post.Category ?? new Category(), Categories = new List(), TocNodes = post.ExtractToc() }; @@ -459,4 +459,4 @@ public async Task> GetLatestPosts(int count = 10) { .Take(count) .ToListAsync(); } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/Services/ThemeService.cs b/src/StarBlog.Web/Services/ThemeService.cs index 711be38..5dc771a 100644 --- a/src/StarBlog.Web/Services/ThemeService.cs +++ b/src/StarBlog.Web/Services/ThemeService.cs @@ -1,4 +1,4 @@ -namespace StarBlog.Web.Services; +namespace StarBlog.Web.Services; public class ThemeService { public const string BootstrapTheme = "Bootstrap"; @@ -26,7 +26,7 @@ public ThemeService(IWebHostEnvironment env) { } public class Theme { - public string Name { get; set; } - public string Path { get; set; } - public string CssUrl { get; set; } -} \ No newline at end of file + public string Name { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string CssUrl { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/Services/TranslationService.cs b/src/StarBlog.Web/Services/TranslationService.cs index 27e90cb..63be91a 100644 --- a/src/StarBlog.Web/Services/TranslationService.cs +++ b/src/StarBlog.Web/Services/TranslationService.cs @@ -35,7 +35,7 @@ public class TranslationService { private readonly IBaseRepository _postRepo; private readonly IBaseRepository _translationRepo; private readonly TranslationConfig _config; - private readonly IChatClient _chatClient; + private readonly IChatClient? _chatClient; public TranslationService( ILogger logger, @@ -49,10 +49,12 @@ public TranslationService( // 初始化 LLM 客户端(OpenAI 兼容接口) if (!string.IsNullOrEmpty(_config.LLM.Key)) { + var endpoint = _config.LLM.Endpoint + ?? throw new InvalidOperationException("Translation:LLM:Endpoint 未配置。"); _chatClient = new OpenAIClient( new ApiKeyCredential(_config.LLM.Key), new OpenAIClientOptions { - Endpoint = new Uri(_config.LLM.Endpoint) + Endpoint = new Uri(endpoint) } ).GetChatClient(_config.LLM.Model).AsIChatClient(); } @@ -221,7 +223,7 @@ private async Task GenerateWithRetry(string prompt) { for (int attempt = 1; attempt <= _config.MaxRetries; attempt++) { try { var sb = new StringBuilder(); - await foreach (var update in _chatClient.GetStreamingResponseAsync(prompt)) { + await foreach (var update in _chatClient!.GetStreamingResponseAsync(prompt)) { if (!string.IsNullOrEmpty(update.Text)) { sb.Append(update.Text); } diff --git a/src/StarBlog.Web/Services/VisitRecordServices/VisitRecordService.cs b/src/StarBlog.Web/Services/VisitRecordServices/VisitRecordService.cs index fafdf71..1ba50b6 100644 --- a/src/StarBlog.Web/Services/VisitRecordServices/VisitRecordService.cs +++ b/src/StarBlog.Web/Services/VisitRecordServices/VisitRecordService.cs @@ -269,12 +269,8 @@ public async Task> GetReferrerDomains(VisitRecordParameters // 在内存中进行 Uri 解析和分组 return records .Select(r => { - try { - return new Uri(r!).Host; - } - catch { - return r; - } // 处理无效的 URL + if (string.IsNullOrWhiteSpace(r)) return string.Empty; + return Uri.TryCreate(r, UriKind.Absolute, out var uri) ? uri.Host : r; }) .GroupBy(host => host) .Select(g => new ReferrerDomain { Domain = g.Key, Count = g.Count() }) @@ -403,9 +399,9 @@ public async Task GetTechDistribution(VisitRecordParameters p) .ToListAsync(); return new TechDistribution { - Browsers = browserStats.Select(b => new NameCountPair { Name = b.Browser, Count = b.Count }).ToList(), - OperatingSystems = osStats.Select(o => new NameCountPair { Name = o.OS, Count = o.Count }).ToList(), - Devices = deviceStats.Select(d => new NameCountPair { Name = d.Device, Count = d.Count }).ToList() + Browsers = browserStats.Select(b => new NameCountPair { Name = b.Browser ?? string.Empty, Count = b.Count }).ToList(), + OperatingSystems = osStats.Select(o => new NameCountPair { Name = o.OS ?? string.Empty, Count = o.Count }).ToList(), + Devices = deviceStats.Select(d => new NameCountPair { Name = d.Device ?? string.Empty, Count = d.Count }).ToList() }; } diff --git a/src/StarBlog.Web/StarBlog.Web.csproj b/src/StarBlog.Web/StarBlog.Web.csproj index d8e095a..76ccc59 100644 --- a/src/StarBlog.Web/StarBlog.Web.csproj +++ b/src/StarBlog.Web/StarBlog.Web.csproj @@ -37,7 +37,7 @@ - + diff --git a/src/StarBlog.Web/ViewModels/Auth/LoginToken.cs b/src/StarBlog.Web/ViewModels/Auth/LoginToken.cs index 14a24e6..f247c5b 100644 --- a/src/StarBlog.Web/ViewModels/Auth/LoginToken.cs +++ b/src/StarBlog.Web/ViewModels/Auth/LoginToken.cs @@ -1,6 +1,6 @@ -namespace StarBlog.Web.ViewModels.Auth; +namespace StarBlog.Web.ViewModels.Auth; public class LoginToken { - public string Token { get; set; } + public string Token { get; set; } = string.Empty; public DateTime Expiration { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Auth/LoginUser.cs b/src/StarBlog.Web/ViewModels/Auth/LoginUser.cs index d2cf3c7..5e93a54 100644 --- a/src/StarBlog.Web/ViewModels/Auth/LoginUser.cs +++ b/src/StarBlog.Web/ViewModels/Auth/LoginUser.cs @@ -1,6 +1,6 @@ -namespace StarBlog.Web.ViewModels.Auth; +namespace StarBlog.Web.ViewModels.Auth; public class LoginUser { - public string Username { get; set; } - public string Password { get; set; } -} \ No newline at end of file + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/ViewModels/Blog/BlogListViewModel.cs b/src/StarBlog.Web/ViewModels/Blog/BlogListViewModel.cs index 7ed3c3d..3cc5600 100644 --- a/src/StarBlog.Web/ViewModels/Blog/BlogListViewModel.cs +++ b/src/StarBlog.Web/ViewModels/Blog/BlogListViewModel.cs @@ -1,4 +1,4 @@ -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.Extensions.WebEncoders.Testing; using StarBlog.Data.Models; @@ -8,16 +8,16 @@ namespace StarBlog.Web.ViewModels.Blog; public class BlogListViewModel { - public string SortType { get; set; } - public string SortBy { get; set; } - public Category CurrentCategory { get; set; } + public string SortType { get; set; } = string.Empty; + public string SortBy { get; set; } = string.Empty; + public Category CurrentCategory { get; set; } = new(); public int CurrentCategoryId { get; set; } - public IPagedList Posts { get; set; } - public List Categories { get; set; } + public IPagedList Posts { get; set; } = new StaticPagedList([], 1, 1, 0); + public List Categories { get; set; } = []; public List? CategoryNodes { get; set; } public string CategoryNodesJson => JsonSerializer.Serialize( CategoryNodes, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping } ); -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Blog/PostUpdateDto.cs b/src/StarBlog.Web/ViewModels/Blog/PostUpdateDto.cs index d9340eb..ca88e66 100644 --- a/src/StarBlog.Web/ViewModels/Blog/PostUpdateDto.cs +++ b/src/StarBlog.Web/ViewModels/Blog/PostUpdateDto.cs @@ -1,12 +1,12 @@ -namespace StarBlog.Web.ViewModels.Blog; +namespace StarBlog.Web.ViewModels.Blog; public class PostUpdateDto { - public string Id { get; set; } + public string Id { get; set; } = string.Empty; /// /// 标题 /// - public string Title { get; set; } + public string Title { get; set; } = string.Empty; /// /// 文章链接,设置后可以通过以下形式访问文章 @@ -27,15 +27,15 @@ public class PostUpdateDto { /// /// 梗概 /// - public string Summary { get; set; } + public string Summary { get; set; } = string.Empty; /// /// 内容(markdown格式) /// - public string Content { get; set; } + public string Content { get; set; } = string.Empty; /// /// 分类ID /// public int CategoryId { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Categories/CategoryCreationDto.cs b/src/StarBlog.Web/ViewModels/Categories/CategoryCreationDto.cs index e25ba34..ddc99cd 100644 --- a/src/StarBlog.Web/ViewModels/Categories/CategoryCreationDto.cs +++ b/src/StarBlog.Web/ViewModels/Categories/CategoryCreationDto.cs @@ -1,7 +1,7 @@ -namespace StarBlog.Web.ViewModels.Categories; +namespace StarBlog.Web.ViewModels.Categories; public record CategoryCreationDto { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public int ParentId { get; set; } public bool Visible { get; set; } = true; -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Categories/CategoryNode.cs b/src/StarBlog.Web/ViewModels/Categories/CategoryNode.cs index 4696849..94656c0 100644 --- a/src/StarBlog.Web/ViewModels/Categories/CategoryNode.cs +++ b/src/StarBlog.Web/ViewModels/Categories/CategoryNode.cs @@ -1,9 +1,9 @@ -namespace StarBlog.Web.ViewModels.Categories; +namespace StarBlog.Web.ViewModels.Categories; public class CategoryNode { public int Id { get; set; } public string? text { get; set; } public string? href { get; set; } - public List tags { get; set; } + public List tags { get; set; } = []; public List? nodes { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto.cs b/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto.cs index 1437bad..1519ec3 100644 --- a/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto.cs +++ b/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto.cs @@ -1,15 +1,15 @@ -namespace StarBlog.Web.ViewModels.Categories; +namespace StarBlog.Web.ViewModels.Categories; public class FeaturedCategoryCreationDto { /// /// 重新定义的推荐名称 /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// 推荐分类解释 /// - public string Description { get; set; } + public string Description { get; set; } = string.Empty; /// /// 图标 @@ -20,5 +20,5 @@ public class FeaturedCategoryCreationDto { /// fa-brands fa-android /// /// - public string IconCssClass { get; set; } -} \ No newline at end of file + public string IconCssClass { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto2.cs b/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto2.cs index 5be4aff..5fd6206 100644 --- a/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto2.cs +++ b/src/StarBlog.Web/ViewModels/Categories/FeaturedCategoryCreationDto2.cs @@ -1,15 +1,15 @@ -namespace StarBlog.Web.ViewModels.Categories; +namespace StarBlog.Web.ViewModels.Categories; public class FeaturedCategoryCreationDto2 { /// /// 重新定义的推荐名称 /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// 推荐分类解释 /// - public string Description { get; set; } + public string Description { get; set; } = string.Empty; /// /// 图标 @@ -20,10 +20,10 @@ public class FeaturedCategoryCreationDto2 { /// fa-brands fa-android /// /// - public string IconCssClass { get; set; } + public string IconCssClass { get; set; } = string.Empty; /// /// 分类ID /// public int CategoryId { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/ColorfulButtonViewModel.cs b/src/StarBlog.Web/ViewModels/ColorfulButtonViewModel.cs index 9e1bdf5..056589f 100644 --- a/src/StarBlog.Web/ViewModels/ColorfulButtonViewModel.cs +++ b/src/StarBlog.Web/ViewModels/ColorfulButtonViewModel.cs @@ -1,4 +1,4 @@ -namespace StarBlog.Web.ViewModels; +namespace StarBlog.Web.ViewModels; public static class LinkTarget { public const string Blank = "_blank"; @@ -8,8 +8,8 @@ public static class LinkTarget { } public class ColorfulButtonViewModel { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public string Url { get; set; } = "#"; public string Target { get; set; } = LinkTarget.Blank; public string? Tooltips { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Comments/CommentCreationDto.cs b/src/StarBlog.Web/ViewModels/Comments/CommentCreationDto.cs index 5ad17c3..a251dd2 100644 --- a/src/StarBlog.Web/ViewModels/Comments/CommentCreationDto.cs +++ b/src/StarBlog.Web/ViewModels/Comments/CommentCreationDto.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace StarBlog.Web.ViewModels.Comments; @@ -6,24 +6,24 @@ public class CommentCreationDto { public string? ParentId { get; set; } [Required] - public string PostId { get; set; } + public string PostId { get; set; } = string.Empty; [MinLength(2, ErrorMessage = "长度在 2 到 20 个字符")] [MaxLength(20, ErrorMessage = "长度在 2 到 20 个字符")] - public string UserName { get; set; } + public string UserName { get; set; } = string.Empty; [Required(ErrorMessage = "邮箱地址不能为空")] [EmailAddress(ErrorMessage = "邮箱格式不正确")] - public string Email { get; set; } + public string Email { get; set; } = string.Empty; [Url(ErrorMessage = "请输入正确的url")] public string? Url { get; set; } [Required(ErrorMessage = "邮箱验证码不能为空")] [StringLength(4, ErrorMessage = "长度为 4 个字符")] - public string EmailOtp { get; set; } + public string EmailOtp { get; set; } = string.Empty; [Required(ErrorMessage = "评论内容不能为空")] [MaxLength(300, ErrorMessage = "评论最大长度为300个字符")] - public string Content { get; set; } -} \ No newline at end of file + public string Content { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/ViewModels/Comments/CommentRejectDto.cs b/src/StarBlog.Web/ViewModels/Comments/CommentRejectDto.cs index 3029e88..2914b6d 100644 --- a/src/StarBlog.Web/ViewModels/Comments/CommentRejectDto.cs +++ b/src/StarBlog.Web/ViewModels/Comments/CommentRejectDto.cs @@ -1,5 +1,5 @@ -namespace StarBlog.Web.ViewModels.Comments; +namespace StarBlog.Web.ViewModels.Comments; public class CommentRejectDto { - public string Reason { get; set; } -} \ No newline at end of file + public string Reason { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/ViewModels/Config/ConfigItemCreationDto.cs b/src/StarBlog.Web/ViewModels/Config/ConfigItemCreationDto.cs index 6075134..fee1ed7 100644 --- a/src/StarBlog.Web/ViewModels/Config/ConfigItemCreationDto.cs +++ b/src/StarBlog.Web/ViewModels/Config/ConfigItemCreationDto.cs @@ -1,5 +1,7 @@ -public class ConfigItemCreationDto { - public string Key { get; set; } - public string Value { get; set; } +namespace StarBlog.Web.ViewModels.Config; + +public class ConfigItemCreationDto { + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; public string? Description { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/Config/ConfigItemUpdateDto.cs b/src/StarBlog.Web/ViewModels/Config/ConfigItemUpdateDto.cs index 9209f75..ec894dc 100644 --- a/src/StarBlog.Web/ViewModels/Config/ConfigItemUpdateDto.cs +++ b/src/StarBlog.Web/ViewModels/Config/ConfigItemUpdateDto.cs @@ -1,4 +1,6 @@ -public class ConfigItemUpdateDto { - public string Value { get; set; } +namespace StarBlog.Web.ViewModels.Config; + +public class ConfigItemUpdateDto { + public string Value { get; set; } = string.Empty; public string? Description { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/ImageViewModel.cs b/src/StarBlog.Web/ViewModels/ImageViewModel.cs index 170bb6e..56bd39b 100644 --- a/src/StarBlog.Web/ViewModels/ImageViewModel.cs +++ b/src/StarBlog.Web/ViewModels/ImageViewModel.cs @@ -1,9 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace StarBlog.Web.ViewModels; public class ImageViewModel { - private readonly string _imageUrl; + private readonly string _imageUrl = string.Empty; public string Id { get; set; } = Guid.NewGuid().ToString(); public string Src { @@ -13,11 +13,11 @@ public string Src { init => _imageUrl = value; } - public string Alt { get; set; } + public string Alt { get; set; } = string.Empty; [Required] public int Width { get; set; } [Required] public int Height { get; set; } public bool UseRandomImage => string.IsNullOrWhiteSpace(_imageUrl); -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/InitViewModel.cs b/src/StarBlog.Web/ViewModels/InitViewModel.cs index 4ffc563..82a57cb 100644 --- a/src/StarBlog.Web/ViewModels/InitViewModel.cs +++ b/src/StarBlog.Web/ViewModels/InitViewModel.cs @@ -1,17 +1,17 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace StarBlog.Web.ViewModels; public class InitViewModel { [Display(Name = "用户名")] - public string Username { get; set; } + public string Username { get; set; } = string.Empty; [Display(Name = "密码")] - public string Password { get; set; } + public string Password { get; set; } = string.Empty; [Display(Name = "博客域名")] - public string Host { get; set; } + public string Host { get; set; } = string.Empty; [Display(Name = "文章默认渲染方式")] - public string DefaultRender { get; set; } -} \ No newline at end of file + public string DefaultRender { get; set; } = string.Empty; +} diff --git a/src/StarBlog.Web/ViewModels/PostViewModel.cs b/src/StarBlog.Web/ViewModels/PostViewModel.cs index 03c7270..5c36595 100644 --- a/src/StarBlog.Web/ViewModels/PostViewModel.cs +++ b/src/StarBlog.Web/ViewModels/PostViewModel.cs @@ -6,17 +6,17 @@ namespace StarBlog.Web.ViewModels; public class PostViewModel { - public string Id { get; set; } - public string Title { get; set; } - public string Summary { get; set; } - public string Content { get; set; } - public string ContentHtml { get; set; } - public string Path { get; set; } + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string ContentHtml { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; public string? Url { get; set; } public DateTime CreationTime { get; set; } public DateTime LastUpdateTime { get; set; } - public Category Category { get; set; } - public List Categories { get; set; } + public Category Category { get; set; } = new(); + public List Categories { get; set; } = []; public List? TocNodes { get; set; } /// diff --git a/src/StarBlog.Web/ViewModels/VisitRecord/ConversionStats.cs b/src/StarBlog.Web/ViewModels/VisitRecord/ConversionStats.cs index 02a6ea6..a7b8ae6 100644 --- a/src/StarBlog.Web/ViewModels/VisitRecord/ConversionStats.cs +++ b/src/StarBlog.Web/ViewModels/VisitRecord/ConversionStats.cs @@ -1,9 +1,9 @@ namespace StarBlog.Web.ViewModels.VisitRecord; public class ConversionStats { - public string TargetPath { get; set; } + public string TargetPath { get; set; } = string.Empty; public int TotalSessions { get; set; } public int ConvertedSessions { get; set; } public double ConversionRate { get; set; } - public List TopEntryPages { get; set; } -} \ No newline at end of file + public List TopEntryPages { get; set; } = []; +} diff --git a/src/StarBlog.Web/ViewModels/VisitRecord/TechDistribution.cs b/src/StarBlog.Web/ViewModels/VisitRecord/TechDistribution.cs index 08749ec..2158af9 100644 --- a/src/StarBlog.Web/ViewModels/VisitRecord/TechDistribution.cs +++ b/src/StarBlog.Web/ViewModels/VisitRecord/TechDistribution.cs @@ -1,12 +1,12 @@ namespace StarBlog.Web.ViewModels.VisitRecord; public class TechDistribution { - public List Browsers { get; set; } - public List OperatingSystems { get; set; } - public List Devices { get; set; } + public List Browsers { get; set; } = []; + public List OperatingSystems { get; set; } = []; + public List Devices { get; set; } = []; } public class NameCountPair { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public int Count { get; set; } -} \ No newline at end of file +} diff --git a/src/StarBlog.Web/ViewModels/VisitRecord/UserAgentFilterParams.cs b/src/StarBlog.Web/ViewModels/VisitRecord/UserAgentFilterParams.cs index e53e351..38ed637 100644 --- a/src/StarBlog.Web/ViewModels/VisitRecord/UserAgentFilterParams.cs +++ b/src/StarBlog.Web/ViewModels/VisitRecord/UserAgentFilterParams.cs @@ -1,7 +1,7 @@ -namespace StarBlog.Web.ViewModels.VisitRecord; +namespace StarBlog.Web.ViewModels.VisitRecord; public class UserAgentFilterParams { - public List OS { get; set; } - public List Device { get; set; } - public List UserAgent { get; set; } -} \ No newline at end of file + public List OS { get; set; } = []; + public List Device { get; set; } = []; + public List UserAgent { get; set; } = []; +} diff --git a/src/StarBlog.Web/Views/Search/Result.cshtml b/src/StarBlog.Web/Views/Search/Result.cshtml index 0c2c0fb..9573fd5 100644 --- a/src/StarBlog.Web/Views/Search/Result.cshtml +++ b/src/StarBlog.Web/Views/Search/Result.cshtml @@ -1,4 +1,4 @@ -@model StarBlog.Web.ViewModels.Search.SearchResultViewModel +@model StarBlog.Web.ViewModels.Search.SearchResultViewModel @{ ViewData["Title"] = $"{Model.Keyword} - 搜索结果"; @@ -445,7 +445,7 @@
- @item.Post.Category.Name + @(item.Post.Category?.Name ?? "未分类") @@ -524,4 +524,4 @@ const savedLayout = localStorage.getItem('searchLayout') || 'grid'; switchLayout(savedLayout); }); - \ No newline at end of file + diff --git a/src/StarBlog.Web/Views/Shared/Widgets/FeaturedPostCard.cshtml b/src/StarBlog.Web/Views/Shared/Widgets/FeaturedPostCard.cshtml index be85200..42f0986 100644 --- a/src/StarBlog.Web/Views/Shared/Widgets/FeaturedPostCard.cshtml +++ b/src/StarBlog.Web/Views/Shared/Widgets/FeaturedPostCard.cshtml @@ -4,7 +4,7 @@
- @Model.Category.Name + @(Model.Category?.Name ?? "未分类")
@Model.Title
@Model.LastUpdateTime.ToShortDateString()

@Model.Summary.Limit(50)

diff --git a/src/StarBlog.Web/gulpfile.js b/src/StarBlog.Web/gulpfile.js index c18e8a7..a374e6c 100644 --- a/src/StarBlog.Web/gulpfile.js +++ b/src/StarBlog.Web/gulpfile.js @@ -1,4 +1,4 @@ -/// +/// "use strict"; //加载使用到的 gulp 插件 @@ -61,7 +61,7 @@ const customLibs = [ {name: 'element-ui/lib/theme-chalk', dist: './node_modules/element-ui/lib/theme-chalk/index.css'}, {name: 'element-ui/lib/theme-chalk/fonts', dist: './node_modules/element-ui/lib/theme-chalk/fonts/*.*'}, {name: 'font-awesome', dist: './node_modules/@fortawesome/fontawesome-free/**/*.*'}, - {name: 'highlight.js', dist: './node_modules/highlight.js/**/*.*'}, + {name: 'highlight', dist: './node_modules/highlight.js/**/*.*'}, {name: 'github-markdown-css', dist: './node_modules/github-markdown-css/*.css'}, {name: 'imagesloaded', dist: './node_modules/imagesloaded/*.js'}, ] @@ -134,4 +134,4 @@ gulp.task("concat", gulp.series(["concat:js", "concat:css"])) gulp.task("auto", () => { gulp.watch(paths.css, gulp.series(["min:css", "concat:css"])); gulp.watch(paths.js, gulp.series(["min:js", "concat:js"])); -}); \ No newline at end of file +}); diff --git a/src/StarBlog.Web/package.json b/src/StarBlog.Web/package.json index 80d7b50..4061d51 100644 --- a/src/StarBlog.Web/package.json +++ b/src/StarBlog.Web/package.json @@ -9,6 +9,7 @@ "author": "DealiAxy", "license": "ISC", "devDependencies": { + "bower": "^1.8.14", "gulp": "^4.0.2", "gulp-changed": "^4.0.3", "gulp-clean-css": "^4.3.0", @@ -29,9 +30,12 @@ "editor.md-ext": "^1.5.4", "element-ui": "^2.15.13", "github-markdown-css": "^5.1.0", + "highlight.js": "^11.11.1", "imagesloaded": "^5.0.0", "jquery": "^3.6.0", "masonry-layout": "^4.2.2", + "popper.js": "^1.16.1", + "prismjs": "^1.30.0", "vue": "^2.6.14" } } diff --git a/tools/BlogImageOptimizer/BlogImageOptimizer.csproj b/tools/BlogImageOptimizer/BlogImageOptimizer.csproj index f2d4f4f..e94fb07 100644 --- a/tools/BlogImageOptimizer/BlogImageOptimizer.csproj +++ b/tools/BlogImageOptimizer/BlogImageOptimizer.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable Linux diff --git a/tools/DataProc/DataProc.csproj b/tools/DataProc/DataProc.csproj index f20b7d6..8fb9cab 100644 --- a/tools/DataProc/DataProc.csproj +++ b/tools/DataProc/DataProc.csproj @@ -54,6 +54,7 @@ + diff --git a/tools/DataProc/src/Data/Models/AuditLog.cs b/tools/DataProc/src/Data/Models/AuditLog.cs index 73d066d..478810d 100644 --- a/tools/DataProc/src/Data/Models/AuditLog.cs +++ b/tools/DataProc/src/Data/Models/AuditLog.cs @@ -11,7 +11,7 @@ public class AuditLog { /// /// 事件类型(例如:登录、登出、数据修改等) /// - public string EventType { get; set; } + public string? EventType { get; set; } /// /// 执行操作的用户名 diff --git a/tools/DataProc/src/Framework/Extensions/EFCoreItegration.cs b/tools/DataProc/src/Framework/Extensions/EFCoreItegration.cs index 7fbae45..6d400ca 100644 --- a/tools/DataProc/src/Framework/Extensions/EFCoreItegration.cs +++ b/tools/DataProc/src/Framework/Extensions/EFCoreItegration.cs @@ -1,5 +1,4 @@ -using DataProc.Data; -using Microsoft.Extensions.DependencyInjection; +using DataProc.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -8,8 +7,10 @@ namespace DataProc.Framework.Extensions; public static class EFCoreItegration { public static void AddDefaultEFCoreItegration(this FluentConsoleApp app) { + var connectionString = app.Configuration.GetConnectionString("SQLite") + ?? throw new InvalidOperationException("缺少 SQLite 连接字符串配置。"); app.Services.AddDbContext(options => { - options.UseSqlite(app.Configuration.GetConnectionString("SQLite")); + options.UseSqlite(connectionString); }); } -} \ No newline at end of file +} diff --git a/tools/DataProc/src/Services/DataInsightScript.cs b/tools/DataProc/src/Services/DataInsightScript.cs index 07b050d..73b0cae 100644 --- a/tools/DataProc/src/Services/DataInsightScript.cs +++ b/tools/DataProc/src/Services/DataInsightScript.cs @@ -1,4 +1,4 @@ -using FluentResults; +using FluentResults; using FreeSql; using Microsoft.Extensions.Logging; using StarBlog.Data.Models; @@ -82,10 +82,11 @@ public Dictionary GetSections(params string[] titles) { } } - private static string ExtractSectionRegex(string content, string title) { + private static string ExtractSectionRegex(string? content, string title) { + if (string.IsNullOrWhiteSpace(content)) return string.Empty; // 匹配目标标题开始,直到下一个标题或文档末尾 string pattern = $@"##\s*{title}\s*([\s\S]*?)(?=\n##|$)"; var match = Regex.Match(content, pattern); return match.Success ? match.Groups[1].Value.Trim() : string.Empty; } -} \ No newline at end of file +} diff --git a/tools/DataProc/src/Services/ImageOptimizer.cs b/tools/DataProc/src/Services/ImageOptimizer.cs index 72979f8..fb54581 100644 --- a/tools/DataProc/src/Services/ImageOptimizer.cs +++ b/tools/DataProc/src/Services/ImageOptimizer.cs @@ -1,4 +1,4 @@ -using FluentResults; +using FluentResults; using FreeSql; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -48,9 +48,9 @@ public class ProcessingStats { } public class ImageOptimizer : IService { - private readonly ILogger logger; - private readonly IConfiguration conf; - private readonly IBaseRepository postRepo; + private readonly ILogger _logger; + private readonly IConfiguration _conf; + private readonly IBaseRepository _postRepo; // 统计信息 - 使用线程安全的集合 private readonly ConcurrentBag _processingStats = new(); @@ -74,9 +74,9 @@ public ImageOptimizer( ILogger logger, IConfiguration conf, IBaseRepository postRepo) { - this.logger = logger; - this.conf = conf; - this.postRepo = postRepo; + _logger = logger; + _conf = conf; + _postRepo = postRepo; // 设置最大并发数,默认为CPU核心数 _maxConcurrency = conf.GetValue("ImageOptimizer:MaxConcurrency", Environment.ProcessorCount); @@ -86,9 +86,9 @@ public ImageOptimizer( _compressionOptions = InitializeCompressionOptions(conf); _compressionModes = _compressionOptions.Modes; - logger.LogInformation("图片压缩器初始化 - 最大并发数: {MaxConcurrency}", _maxConcurrency); - logger.LogInformation("默认压缩模式: {DefaultMode}", _compressionOptions.DefaultMode); - logger.LogInformation("可用压缩模式: {ModeCount} 种", _compressionModes.Count); + _logger.LogInformation("图片压缩器初始化 - 最大并发数: {MaxConcurrency}", _maxConcurrency); + _logger.LogInformation("默认压缩模式: {DefaultMode}", _compressionOptions.DefaultMode); + _logger.LogInformation("可用压缩模式: {ModeCount} 种", _compressionModes.Count); // 显示压缩模式详情 LogCompressionModes(); @@ -177,7 +177,7 @@ private void ApplyImageResize(Image image, CompressionMode mode) { var newWidth = (int)(image.Width * scale); var newHeight = (int)(image.Height * scale); - logger.LogDebug("调整图片尺寸: {OriginalWidth}×{OriginalHeight} -> {NewWidth}×{NewHeight} (模式: {ModeName})", + _logger.LogDebug("调整图片尺寸: {OriginalWidth}×{OriginalHeight} -> {NewWidth}×{NewHeight} (模式: {ModeName})", image.Width, image.Height, newWidth, newHeight, mode.Name); image.Mutate(x => x.Resize(newWidth, newHeight)); @@ -187,44 +187,44 @@ private void ApplyImageResize(Image image, CompressionMode mode) { /// 记录压缩模式配置信息 /// private void LogCompressionModes() { - logger.LogInformation(""); - logger.LogInformation("🎨 压缩模式配置:"); + _logger.LogInformation(""); + _logger.LogInformation("🎨 压缩模式配置:"); foreach (var (key, mode) in _compressionModes) { var isDefault = key == _compressionOptions.DefaultMode ? " [默认]" : ""; var sizeInfo = mode.MaxWidth == int.MaxValue ? "保持原尺寸" : $"{mode.MaxWidth}×{mode.MaxHeight}"; - logger.LogInformation(" • {ModeName}{IsDefault}: {SizeInfo}, 质量{Quality}%", + _logger.LogInformation(" • {ModeName}{IsDefault}: {SizeInfo}, 质量{Quality}%", mode.Name, isDefault, sizeInfo, mode.Quality); - logger.LogInformation(" {Description}", mode.Description); + _logger.LogInformation(" {Description}", mode.Description); } - logger.LogInformation(""); + _logger.LogInformation(""); } public async Task Run() { - var posts = await postRepo.Select.ToListAsync(); - var wwwroot = conf.GetValue("StarBlog:wwwroot"); + var posts = await _postRepo.Select.ToListAsync(); + var wwwroot = _conf.GetValue("StarBlog:wwwroot"); if (string.IsNullOrWhiteSpace(wwwroot)) { throw new Exception("wwwroot 配置错误"); } // 获取输出目录配置 - var outputBaseDir = conf.GetValue("ImageOptimizer:OutputDir"); + var outputBaseDir = _conf.GetValue("ImageOptimizer:OutputDir"); if (string.IsNullOrWhiteSpace(outputBaseDir)) { throw new Exception("未配置输出目录"); } - logger.LogInformation("压缩后图片将保存到: {OutputBaseDir}", outputBaseDir); + _logger.LogInformation("压缩后图片将保存到: {OutputBaseDir}", outputBaseDir); // 确保输出基础目录存在 Directory.CreateDirectory(outputBaseDir); - logger.LogInformation("开始处理 {TotalPosts} 篇文章的图片", posts.Count); + _logger.LogInformation("开始处理 {TotalPosts} 篇文章的图片", posts.Count); var processedCount = 0; foreach (var post in posts) { processedCount++; - logger.LogInformation("处理进度: {Current}/{Total} - 文章 {PostId}", + _logger.LogInformation("处理进度: {Current}/{Total} - 文章 {PostId}", processedCount, posts.Count, post.Id); var blogImageDir = Path.Combine(wwwroot, "media", "blog", post.Id); @@ -236,7 +236,7 @@ public async Task Run() { var outputDir = Path.Combine(outputBaseDir, post.Id); Directory.CreateDirectory(outputDir); - logger.LogInformation("处理文章 {PostId} 的图片, 源目录: {SourceDir}, 输出目录: {OutputDir}", + _logger.LogInformation("处理文章 {PostId} 的图片, 源目录: {SourceDir}, 输出目录: {OutputDir}", post.Id, blogImageDir, outputDir); var files = Directory.GetFiles(blogImageDir); @@ -253,7 +253,7 @@ public async Task Run() { var compressionTasks = imageFiles.Select(async file => { await _concurrencyLimiter.WaitAsync(); try { - logger.LogInformation("处理图片 {FileName}", file); + _logger.LogInformation("处理图片 {FileName}", file); Interlocked.Increment(ref _processedImages); var result = await CompressImage(file, outputDir); @@ -279,7 +279,7 @@ public async Task Run() { var outputFormat = Path.GetExtension(result.NewFilePath).ToLower(); _formatStats.AddOrUpdate(outputFormat, 1, (key, value) => value + 1); - logger.LogInformation("图片压缩成功: {OriginalFile} -> {NewFile}, 压缩率: {CompressionRatio:P2}", + _logger.LogInformation("图片压缩成功: {OriginalFile} -> {NewFile}, 压缩率: {CompressionRatio:P2}", originalFileName, newFileName, result.CompressionRatio); } else { Interlocked.Increment(ref _failedCompressions); @@ -288,7 +288,7 @@ public async Task Run() { return result; } catch (Exception ex) { - logger.LogError(ex, "压缩图片失败: {FileName}", file); + _logger.LogError(ex, "压缩图片失败: {FileName}", file); Interlocked.Increment(ref _failedCompressions); return null; } @@ -301,7 +301,7 @@ public async Task Run() { var results = await Task.WhenAll(compressionTasks); // 收集文章级别的统计信息 - foreach (var result in results.Where(r => r != null)) { + foreach (var result in results.OfType()) { postStats.OriginalSize += result.OriginalSize; postStats.CompressedSize += result.CompressedSize; postStats.ProcessedImages++; @@ -324,12 +324,12 @@ public async Task Run() { var updatedContent = UpdateMarkdownImageLinks(post, fileNameMappings); if (updatedContent != post.Content) { post.Content = updatedContent; - await postRepo.UpdateAsync(post); - logger.LogInformation("已更新文章 {PostId} 的图片链接", post.Id); + await _postRepo.UpdateAsync(post); + _logger.LogInformation("已更新文章 {PostId} 的图片链接", post.Id); } } catch (Exception ex) { - logger.LogError(ex, "更新文章 {PostId} 的图片链接失败", post.Id); + _logger.LogError(ex, "更新文章 {PostId} 的图片链接失败", post.Id); // 数据库更新失败不影响继续处理其他文章 } } @@ -345,37 +345,37 @@ public async Task Run() { /// 生成汇总报告 ///
private void GenerateSummaryReport(string outputBaseDir, string wwwroot) { - logger.LogInformation(""); - logger.LogInformation("🎉 ================ 图片压缩汇总报告 ================"); - logger.LogInformation(""); + _logger.LogInformation(""); + _logger.LogInformation("🎉 ================ 图片压缩汇总报告 ================"); + _logger.LogInformation(""); // 基本统计 - logger.LogInformation("📊 基本统计:"); - logger.LogInformation(" • 处理的文章数量: {ProcessedPosts}", _processingStats.Count); - logger.LogInformation(" • 发现的图片总数: {TotalImages}", _totalImages); - logger.LogInformation(" • 处理的图片数量: {ProcessedImages}", _processedImages); - logger.LogInformation(" • 成功压缩数量: {SuccessfulCompressions}", _successfulCompressions); - logger.LogInformation(" • 压缩失败数量: {FailedCompressions}", _failedCompressions); - logger.LogInformation(""); + _logger.LogInformation("📊 基本统计:"); + _logger.LogInformation(" • 处理的文章数量: {ProcessedPosts}", _processingStats.Count); + _logger.LogInformation(" • 发现的图片总数: {TotalImages}", _totalImages); + _logger.LogInformation(" • 处理的图片数量: {ProcessedImages}", _processedImages); + _logger.LogInformation(" • 成功压缩数量: {SuccessfulCompressions}", _successfulCompressions); + _logger.LogInformation(" • 压缩失败数量: {FailedCompressions}", _failedCompressions); + _logger.LogInformation(""); // 文件大小统计 var totalSavedBytes = _totalOriginalSize - _totalCompressedSize; var overallCompressionRatio = _totalOriginalSize > 0 ? 1.0 - (double)_totalCompressedSize / _totalOriginalSize : 0; - logger.LogInformation("💾 文件大小统计:"); - logger.LogInformation(" • 原始总大小: {OriginalSize}", FormatFileSize(_totalOriginalSize)); - logger.LogInformation(" • 压缩后总大小: {CompressedSize}", FormatFileSize(_totalCompressedSize)); - logger.LogInformation(" • 节省空间: {SavedSize}", FormatFileSize(totalSavedBytes)); - logger.LogInformation(" • 总体压缩率: {CompressionRatio:P2}", overallCompressionRatio); - logger.LogInformation(""); + _logger.LogInformation("💾 文件大小统计:"); + _logger.LogInformation(" • 原始总大小: {OriginalSize}", FormatFileSize(_totalOriginalSize)); + _logger.LogInformation(" • 压缩后总大小: {CompressedSize}", FormatFileSize(_totalCompressedSize)); + _logger.LogInformation(" • 节省空间: {SavedSize}", FormatFileSize(totalSavedBytes)); + _logger.LogInformation(" • 总体压缩率: {CompressionRatio:P2}", overallCompressionRatio); + _logger.LogInformation(""); // 格式统计 if (_formatStats.Count > 0) { - logger.LogInformation("📁 输出格式统计:"); + _logger.LogInformation("📁 输出格式统计:"); foreach (var format in _formatStats.OrderByDescending(x => x.Value)) { - logger.LogInformation(" • {Format}: {Count} 个文件", format.Key.ToUpper(), format.Value); + _logger.LogInformation(" • {Format}: {Count} 个文件", format.Key.ToUpper(), format.Value); } - logger.LogInformation(""); + _logger.LogInformation(""); } // 前10个压缩效果最好的文章 @@ -386,44 +386,44 @@ private void GenerateSummaryReport(string outputBaseDir, string wwwroot) { .ToList(); if (topCompressionPosts.Count > 0) { - logger.LogInformation("🏆 压缩效果最佳的文章 (前10名):"); + _logger.LogInformation("🏆 压缩效果最佳的文章 (前10名):"); for (int i = 0; i < topCompressionPosts.Count; i++) { var post = topCompressionPosts[i]; - logger.LogInformation(" {Rank}. 文章 {PostId}: 节省 {SavedSize}, 压缩率 {CompressionRatio:P2} ({SuccessfulCount}/{TotalCount} 张图片)", + _logger.LogInformation(" {Rank}. 文章 {PostId}: 节省 {SavedSize}, 压缩率 {CompressionRatio:P2} ({SuccessfulCount}/{TotalCount} 张图片)", i + 1, post.PostId, FormatFileSize(post.SavedBytes), post.CompressionRatio, post.SuccessfulCompressions, post.TotalImages); } - logger.LogInformation(""); + _logger.LogInformation(""); } // 失败统计 var failedPosts = _processingStats.Where(p => p.FailedCompressions > 0).ToList(); if (failedPosts.Count > 0) { - logger.LogInformation("⚠️ 压缩失败统计:"); + _logger.LogInformation("⚠️ 压缩失败统计:"); foreach (var post in failedPosts.OrderByDescending(p => p.FailedCompressions)) { - logger.LogInformation(" • 文章 {PostId}: {FailedCount} 张图片压缩失败", + _logger.LogInformation(" • 文章 {PostId}: {FailedCount} 张图片压缩失败", post.PostId, post.FailedCompressions); } - logger.LogInformation(""); + _logger.LogInformation(""); } // 目录信息 - logger.LogInformation("📂 目录信息:"); - logger.LogInformation(" • 原始图片目录: {OriginalDir}", Path.Combine(wwwroot, "media", "blog")); - logger.LogInformation(" • 压缩后图片目录: {OutputDir}", outputBaseDir); - logger.LogInformation(""); + _logger.LogInformation("📂 目录信息:"); + _logger.LogInformation(" • 原始图片目录: {OriginalDir}", Path.Combine(wwwroot, "media", "blog")); + _logger.LogInformation(" • 压缩后图片目录: {OutputDir}", outputBaseDir); + _logger.LogInformation(""); // 操作建议 - logger.LogInformation("💡 下一步操作建议:"); - logger.LogInformation(" 1. 检查压缩后的图片质量和效果"); - logger.LogInformation(" 2. 确认无误后,可以手动替换原目录中的图片文件"); - logger.LogInformation(" 3. 建议先备份原始图片目录"); + _logger.LogInformation("💡 下一步操作建议:"); + _logger.LogInformation(" 1. 检查压缩后的图片质量和效果"); + _logger.LogInformation(" 2. 确认无误后,可以手动替换原目录中的图片文件"); + _logger.LogInformation(" 3. 建议先备份原始图片目录"); if (failedPosts.Count > 0) { - logger.LogInformation(" 4. 检查压缩失败的图片,可能需要手动处理"); + _logger.LogInformation(" 4. 检查压缩失败的图片,可能需要手动处理"); } - logger.LogInformation(""); - logger.LogInformation("🎉 ================ 报告结束 ================"); - logger.LogInformation(""); + _logger.LogInformation(""); + _logger.LogInformation("🎉 ================ 报告结束 ================"); + _logger.LogInformation(""); } /// @@ -496,7 +496,7 @@ private async Task CompressImage(string imagePath, string out }; } - logger.LogDebug("选择的输出格式: {OutputFormat}", outputFormat); + _logger.LogDebug("选择的输出格式: {OutputFormat}", outputFormat); return new CompressionResult { Success = true, @@ -531,7 +531,7 @@ private async Task CompressImage(string imagePath, string out bool hasTransparency = HasTransparency(image); bool isSimpleGraphic = IsSimpleGraphic(image); - logger.LogDebug("图片分析 - 透明度: {HasTransparency}, 图片类型: {ImageType}", + _logger.LogDebug("图片分析 - 透明度: {HasTransparency}, 图片类型: {ImageType}", hasTransparency, isSimpleGraphic ? "图形/图标" : "照片/复杂图像"); // 智能选择格式 @@ -588,7 +588,7 @@ private async Task CompressImage(string imagePath, string out var webpSize = new FileInfo(tempWebp).Length; var jpegSize = new FileInfo(tempJpeg).Length; - logger.LogDebug("格式比较 - WebP: {WebpSize} bytes, JPEG: {JpegSize} bytes", webpSize, jpegSize); + _logger.LogDebug("格式比较 - WebP: {WebpSize} bytes, JPEG: {JpegSize} bytes", webpSize, jpegSize); // 选择更小的格式 if (webpSize <= jpegSize) { @@ -614,8 +614,9 @@ private async Task CompressImage(string imagePath, string out /// private static bool HasTransparency(Image image) { // 检查像素格式是否支持透明度 + var pixelTypeText = image.PixelType.ToString() ?? string.Empty; return image.PixelType.BitsPerPixel == 32 || - image.PixelType.ToString().Contains("Rgba"); + pixelTypeText.Contains("Rgba", StringComparison.OrdinalIgnoreCase); } /// @@ -662,7 +663,7 @@ private string UpdateMarkdownImageLinks(Post post, Dictionary fi linkInline.Url = Path.Combine(directory, newFileName).Replace('\\', '/'); } - logger.LogDebug("更新图片链接: {OldUrl} -> {NewUrl}", imgUrl, linkInline.Url); + _logger.LogDebug("更新图片链接: {OldUrl} -> {NewUrl}", imgUrl, linkInline.Url); } } } @@ -684,4 +685,4 @@ public class CompressionResult { public long OriginalSize { get; set; } public long CompressedSize { get; set; } public double CompressionRatio { get; set; } -} \ No newline at end of file +} diff --git a/tools/DataProc/src/Services/LLM.cs b/tools/DataProc/src/Services/LLM.cs index e3e432d..35246fd 100644 --- a/tools/DataProc/src/Services/LLM.cs +++ b/tools/DataProc/src/Services/LLM.cs @@ -1,4 +1,4 @@ -using System.ClientModel; +using System.ClientModel; using DataProc.Entities; using FluentResults; using Microsoft.Extensions.AI; @@ -10,12 +10,14 @@ namespace DataProc.Services; public class LLM : IService { public LLM(IOptions llmOptions) { var llmConfig = llmOptions.Value; + var endpoint = llmConfig.Endpoint + ?? throw new InvalidOperationException("LLM Endpoint 未配置。"); ChatClient = new OpenAIClient( - new ApiKeyCredential(llmConfig.Key), + new ApiKeyCredential(llmConfig.Key ?? throw new InvalidOperationException("LLM Key 未配置。")), new OpenAIClientOptions { - Endpoint = new Uri(llmConfig.Endpoint) + Endpoint = new Uri(endpoint) } - ).GetChatClient(llmConfig.Model).AsIChatClient(); + ).GetChatClient(llmConfig.Model ?? throw new InvalidOperationException("LLM Model 未配置。")).AsIChatClient(); } public IChatClient ChatClient { get; } @@ -81,4 +83,4 @@ public IAsyncEnumerable GenerateChatReplyStreamAsync(params throw new Exception($"AI聊天回复流生成失败: {ex.Message}", ex); } } -} \ No newline at end of file +} diff --git a/tools/StarBlogBackup/StarBlog.BackupTool.csproj b/tools/StarBlogBackup/StarBlog.BackupTool.csproj index 8bbc4f2..2bff5f4 100644 --- a/tools/StarBlogBackup/StarBlog.BackupTool.csproj +++ b/tools/StarBlogBackup/StarBlog.BackupTool.csproj @@ -10,6 +10,7 @@ +