diff --git a/QYZH.InteractiveMagazine.IService/IBannerService.cs b/QYZH.InteractiveMagazine.IService/IBannerService.cs new file mode 100644 index 0000000..4c76041 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IBannerService.cs @@ -0,0 +1,61 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Banner; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 轮播图服务接口 +/// +public interface IBannerService : IBaseService +{ + /// + /// 创建轮播图 + /// + /// 轮播图信息 + /// 创建后的轮播图信息 + Task CreateAsync(BannerInput input); + + /// + /// 更新轮播图 + /// + /// 轮播图ID + /// 轮播图信息 + /// 更新后的轮播图信息 + Task UpdateAsync(long id, BannerInput input); + + /// + /// 删除轮播图 + /// + /// 轮播图ID + Task DeleteAsync(long id); + + /// + /// 根据ID获取轮播图 + /// + /// 轮播图ID + /// 轮播图信息 + Task GetByIdAsync(long id); + + /// + /// 分页查询轮播图列表 + /// + /// 查询条件 + /// 分页结果 + Task> GetListAsync(BannerQueryInput input); + + /// + /// 更新轮播图启用/禁用状态 + /// + /// 轮播图ID + /// 操作结果 + Task UpdateStatusAsync(long id); + + /// + /// 获取启用轮播图列表 + /// + /// 展示位置 + /// 轮播图列表 + Task> GetEnabledListAsync(BannerPositionEnum position); +} diff --git a/QYZH.InteractiveMagazine.IService/IMaterialService.cs b/QYZH.InteractiveMagazine.IService/IMaterialService.cs new file mode 100644 index 0000000..7cd4315 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IMaterialService.cs @@ -0,0 +1,59 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Material; +using MaterialEntity = QYZH.InteractiveMagazine.Models.Entity.Material; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 资料服务接口 +/// +public interface IMaterialService : IBaseService +{ + /// + /// 创建资料 + /// + /// 资料信息 + /// 创建后的资料信息 + Task CreateAsync(MaterialInput input); + + /// + /// 更新资料 + /// + /// 资料ID + /// 资料信息 + /// 更新后的资料信息 + Task UpdateAsync(long id, MaterialInput input); + + /// + /// 删除资料 + /// + /// 资料ID + Task DeleteAsync(long id); + + /// + /// 根据ID获取资料 + /// + /// 资料ID + /// 资料信息 + Task GetByIdAsync(long id); + + /// + /// 分页查询资料列表 + /// + /// 查询条件 + /// 分页结果 + Task> GetListAsync(MaterialQueryInput input); + + /// + /// 更新资料启用/禁用状态 + /// + /// 资料ID + /// 操作结果 + Task UpdateStatusAsync(long id); + + /// + /// 获取小程序可见资料列表 + /// + /// 资料列表 + Task> GetEnabledListAsync(); +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Banner/BannerDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Banner/BannerDto.cs new file mode 100644 index 0000000..3aeec81 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Banner/BannerDto.cs @@ -0,0 +1,163 @@ +using QYZH.InteractiveMagazine.Common.Helpers; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.Models.Dto.Banner; + +/// +/// 轮播图创建/更新输入 +/// +public class BannerInput +{ + /// + /// 标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 轮播图OSS路径 + /// + public string ImageUrl { get; set; } = string.Empty; + + /// + /// 展示位置 + /// + public BannerPositionEnum Position { get; set; } = BannerPositionEnum.Home; + + /// + /// 跳转类型 + /// + public BannerTargetTypeEnum TargetType { get; set; } = BannerTargetTypeEnum.None; + + /// + /// 跳转值 + /// + public string? TargetValue { get; set; } + + /// + /// 排序值,越小越靠前 + /// + public int Sort { get; set; } + + /// + /// 生效开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 生效结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } +} + +/// +/// 轮播图输出 +/// +public class BannerOutput +{ + private string _imageUrl = string.Empty; + + /// + /// 主键ID + /// + public long Id { get; set; } + + /// + /// 标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 轮播图完整地址 + /// + public string ImageUrl + { + get => DomainHelper.OssFullUrl(_imageUrl); + set => _imageUrl = value ?? string.Empty; + } + + /// + /// 展示位置 + /// + public BannerPositionEnum Position { get; set; } + + /// + /// 跳转类型 + /// + public BannerTargetTypeEnum TargetType { get; set; } + + /// + /// 跳转值 + /// + public string? TargetValue { get; set; } + + /// + /// 排序值,越小越靠前 + /// + public int Sort { get; set; } + + /// + /// 生效开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 生效结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } + + /// + /// 创建人 + /// + public string? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 修改人 + /// + public string? UpdatedBy { get; set; } + + /// + /// 修改时间 + /// + public DateTime? UpdatedAt { get; set; } +} + +/// +/// 轮播图分页查询输入 +/// +public class BannerQueryInput : PageQueryModel +{ + /// + /// 标题 + /// + public string? Title { get; set; } + + /// + /// 展示位置 + /// + public BannerPositionEnum? Position { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Material/MaterialDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Material/MaterialDto.cs new file mode 100644 index 0000000..1f5a130 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Material/MaterialDto.cs @@ -0,0 +1,200 @@ +using QYZH.InteractiveMagazine.Common.Helpers; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.Models.Dto.Material; + +/// +/// 资料创建/更新输入 +/// +public class MaterialInput +{ + /// + /// 资料标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 资料名称 + /// + public string? Name { get; set; } + + /// + /// 资料描述 + /// + public string? Description { get; set; } + + /// + /// 排序值,越小越靠前 + /// + public int Sort { get; set; } + + /// + /// 生效开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 生效结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 资料文件列表 + /// + public List Files { get; set; } = new(); +} + +/// +/// 资料文件输入 +/// +public class MaterialFileInput +{ + /// + /// 文件类型 + /// + public MaterialFileTypeEnum FileType { get; set; } + + /// + /// 文件OSS路径 + /// + public string FileUrl { get; set; } = string.Empty; + + /// + /// 文件名称 + /// + public string? FileName { get; set; } + + /// + /// 排序值,越小越靠前 + /// + public int Sort { get; set; } +} + +/// +/// 资料输出 +/// +public class MaterialOutput +{ + /// + /// 主键ID + /// + public long Id { get; set; } + + /// + /// 资料标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 资料名称 + /// + public string? Name { get; set; } + + /// + /// 资料描述 + /// + public string? Description { get; set; } + + /// + /// 排序值,越小越靠前 + /// + public int Sort { get; set; } + + /// + /// 生效开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 生效结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } + + /// + /// 创建人 + /// + public string? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 修改人 + /// + public string? UpdatedBy { get; set; } + + /// + /// 修改时间 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 资料文件列表 + /// + public List Files { get; set; } = new(); +} + +/// +/// 资料文件输出 +/// +public class MaterialFileOutput +{ + private string _fileUrl = string.Empty; + + /// + /// 主键ID + /// + public long Id { get; set; } + + /// + /// 文件类型 + /// + public MaterialFileTypeEnum FileType { get; set; } + + /// + /// 文件完整地址 + /// + public string FileUrl + { + get => DomainHelper.OssFullUrl(_fileUrl); + set => _fileUrl = value ?? string.Empty; + } + + /// + /// 文件名称 + /// + public string? FileName { get; set; } + + /// + /// 排序值,越小越靠前 + /// + public int Sort { get; set; } +} + +/// +/// 资料分页查询输入 +/// +public class MaterialQueryInput : PageQueryModel +{ + /// + /// 标题 + /// + public string? Title { get; set; } + + /// + /// 名称 + /// + public string? Name { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/Banner.cs b/QYZH.InteractiveMagazine.Models/Entity/Banner.cs new file mode 100644 index 0000000..58f26da --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/Banner.cs @@ -0,0 +1,74 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 轮播图表 +/// +[SugarTable("Banner")] +public partial class Banner : SqlSugarBaseEntity +{ + /// + /// Desc:标题 + /// Default: + /// Nullable:False + /// + public string Title { get; set; } = string.Empty; + + /// + /// Desc:轮播图OSS路径 + /// Default: + /// Nullable:False + /// + public string ImageUrl { get; set; } = string.Empty; + + /// + /// Desc:展示位置 + /// Default:1 + /// Nullable:False + /// + public BannerPositionEnum Position { get; set; } = BannerPositionEnum.Home; + + /// + /// Desc:跳转类型 + /// Default:0 + /// Nullable:False + /// + public BannerTargetTypeEnum TargetType { get; set; } = BannerTargetTypeEnum.None; + + /// + /// Desc:跳转值 + /// Default: + /// Nullable:True + /// + public string? TargetValue { get; set; } + + /// + /// Desc:排序值,越小越靠前 + /// Default:0 + /// Nullable:False + /// + public int Sort { get; set; } + + /// + /// Desc:生效开始时间 + /// Default: + /// Nullable:True + /// + public DateTime? StartTime { get; set; } + + /// + /// Desc:生效结束时间 + /// Default: + /// Nullable:True + /// + public DateTime? EndTime { get; set; } + + /// + /// Desc:备注 + /// Default: + /// Nullable:True + /// + public string? Remark { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/Material.cs b/QYZH.InteractiveMagazine.Models/Entity/Material.cs new file mode 100644 index 0000000..3907dba --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/Material.cs @@ -0,0 +1,52 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 资料表 +/// +[SugarTable("Material")] +public partial class Material : SqlSugarBaseEntity +{ + /// + /// Desc:资料标题 + /// Default: + /// Nullable:False + /// + public string Title { get; set; } = string.Empty; + + /// + /// Desc:资料名称 + /// Default: + /// Nullable:True + /// + public string? Name { get; set; } + + /// + /// Desc:资料描述 + /// Default: + /// Nullable:True + /// + public string? Description { get; set; } + + /// + /// Desc:排序值,越小越靠前 + /// Default:0 + /// Nullable:False + /// + public int Sort { get; set; } + + /// + /// Desc:生效开始时间 + /// Default: + /// Nullable:True + /// + public DateTime? StartTime { get; set; } + + /// + /// Desc:生效结束时间 + /// Default: + /// Nullable:True + /// + public DateTime? EndTime { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/MaterialFile.cs b/QYZH.InteractiveMagazine.Models/Entity/MaterialFile.cs new file mode 100644 index 0000000..c035b69 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/MaterialFile.cs @@ -0,0 +1,46 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 资料文件表 +/// +[SugarTable("MaterialFile")] +public partial class MaterialFile : SqlSugarBaseEntity +{ + /// + /// Desc:资料ID + /// Default: + /// Nullable:False + /// + public long MaterialId { get; set; } + + /// + /// Desc:文件类型 + /// Default: + /// Nullable:False + /// + public MaterialFileTypeEnum FileType { get; set; } + + /// + /// Desc:文件OSS路径 + /// Default: + /// Nullable:False + /// + public string FileUrl { get; set; } = string.Empty; + + /// + /// Desc:文件名称 + /// Default: + /// Nullable:True + /// + public string? FileName { get; set; } + + /// + /// Desc:排序值,越小越靠前 + /// Default:0 + /// Nullable:False + /// + public int Sort { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Enum/BannerPositionEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/BannerPositionEnum.cs new file mode 100644 index 0000000..dc6e4ff --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/BannerPositionEnum.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace QYZH.InteractiveMagazine.Models.Enum; + +/// +/// 轮播图展示位置枚举 +/// +public enum BannerPositionEnum +{ + /// + /// 首页 + /// + [Description("首页")] + Home = 1 +} diff --git a/QYZH.InteractiveMagazine.Models/Enum/BannerTargetTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/BannerTargetTypeEnum.cs new file mode 100644 index 0000000..b143303 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/BannerTargetTypeEnum.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; + +namespace QYZH.InteractiveMagazine.Models.Enum; + +/// +/// 轮播图跳转类型枚举 +/// +public enum BannerTargetTypeEnum +{ + /// + /// 无跳转 + /// + [Description("无跳转")] + None = 0, + + /// + /// 链接 + /// + [Description("链接")] + Url = 1, + + /// + /// 期刊 + /// + [Description("期刊")] + Journal = 2, + + /// + /// 商品 + /// + [Description("商品")] + Product = 3 +} diff --git a/QYZH.InteractiveMagazine.Models/Enum/MaterialFileTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/MaterialFileTypeEnum.cs new file mode 100644 index 0000000..9ad34f4 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/MaterialFileTypeEnum.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; + +namespace QYZH.InteractiveMagazine.Models.Enum; + +/// +/// 资料文件类型枚举 +/// +public enum MaterialFileTypeEnum +{ + /// + /// 视频 + /// + [Description("视频")] + Video = 1, + + /// + /// 图片 + /// + [Description("图片")] + Image = 2, + + /// + /// 音频 + /// + [Description("音频")] + Audio = 3 +} diff --git a/QYZH.InteractiveMagazine.Service/BannerService.cs b/QYZH.InteractiveMagazine.Service/BannerService.cs new file mode 100644 index 0000000..094aae1 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/BannerService.cs @@ -0,0 +1,244 @@ +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Infrastructure.OSS; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Banner; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 轮播图服务实现 +/// +public class BannerService( + BaseRepository bannerRepository, + OssService ossService, + ILogger logger) : BaseRepository, IBannerService +{ + /// + /// 创建轮播图 + /// + public async Task CreateAsync(BannerInput input) + { + ValidateInput(input); + + var banner = new Banner + { + Title = input.Title.Trim(), + ImageUrl = input.ImageUrl.Trim(), + Position = input.Position, + TargetType = input.TargetType, + TargetValue = NormalizeTargetValue(input), + Sort = input.Sort, + StartTime = input.StartTime, + EndTime = input.EndTime, + Remark = input.Remark, + Status = (int)DefaultStatusEnum.Active, + IsDeleted = false, + CreatedBy = "System", + CreatedAt = DateTime.Now, + UpdatedBy = "System", + UpdatedAt = DateTime.Now + }; + + var result = await bannerRepository.InsertAsync(banner); + BusinessException.ThrowIf(!result, "创建轮播图失败", ResultCode.GLOBAL_ERROR); + + if (OssImageHelper.IsTempImage(banner.ImageUrl)) + { + var helper = CreateImageHelper(); + banner.ImageUrl = await helper.MoveToFormalAsync(banner.ImageUrl, GetBannerFormalFolder(banner.Position, banner.Id)); + await bannerRepository.UpdateAsync(banner); + } + + logger.LogInformation("轮播图创建成功:{Id}", banner.Id); + return MapToOutput(banner); + } + + /// + /// 更新轮播图 + /// + public async Task UpdateAsync(long id, BannerInput input) + { + ValidateInput(input); + + var banner = await GetValidBannerAsync(id); + var newImageUrl = input.ImageUrl.Trim(); + if (OssImageHelper.IsTempImage(newImageUrl)) + { + var helper = CreateImageHelper(); + newImageUrl = await helper.MoveToFormalAsync(newImageUrl, GetBannerFormalFolder(input.Position, banner.Id)); + } + + banner.Title = input.Title.Trim(); + banner.ImageUrl = newImageUrl; + banner.Position = input.Position; + banner.TargetType = input.TargetType; + banner.TargetValue = NormalizeTargetValue(input); + banner.Sort = input.Sort; + banner.StartTime = input.StartTime; + banner.EndTime = input.EndTime; + banner.Remark = input.Remark; + banner.UpdatedBy = "System"; + banner.UpdatedAt = DateTime.Now; + + var result = await bannerRepository.UpdateAsync(banner); + BusinessException.ThrowIf(!result, "更新轮播图失败", ResultCode.GLOBAL_ERROR); + + logger.LogInformation("轮播图更新成功:{Id}", banner.Id); + return MapToOutput(banner); + } + + /// + /// 删除轮播图 + /// + public async Task DeleteAsync(long id) + { + await GetValidBannerAsync(id); + + var result = await bannerRepository.Context.Updateable() + .SetColumns(b => new Banner + { + IsDeleted = true, + UpdatedBy = "System", + UpdatedAt = DateTime.Now + }) + .Where(b => b.Id == id && !b.IsDeleted) + .ExecuteCommandAsync(); + + BusinessException.ThrowIf(result <= 0, "删除轮播图失败", ResultCode.GLOBAL_ERROR); + logger.LogInformation("轮播图删除成功:{Id}", id); + } + + /// + /// 根据ID获取轮播图 + /// + public async Task GetByIdAsync(long id) + { + var banner = await GetValidBannerAsync(id); + return MapToOutput(banner); + } + + /// + /// 分页查询轮播图列表 + /// + public async Task> GetListAsync(BannerQueryInput input) + { + BusinessException.ThrowIf(input.PageIndex <= 0, "页码必须大于0", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.PageSize <= 0 || input.PageSize > 100, "每页条数必须在1-100之间", ResultCode.BAD_REQUEST); + + RefAsync totalNumber = 0; + var banners = await bannerRepository.Queryable() + .Where(b => !b.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), b => b.Title.Contains(input.Title!.Trim())) + .WhereIF(input.Position.HasValue, b => b.Position == input.Position!.Value) + .WhereIF(input.Status.HasValue, b => b.Status == input.Status!.Value) + .OrderBy(b => b.Sort) + .OrderByDescending(b => b.CreatedAt) + .ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); + + var result = banners.Select(MapToOutput).ToList(); + return new PageListModel(result, input.PageIndex, input.PageSize, totalNumber); + } + + /// + /// 更新轮播图启用/禁用状态 + /// + public async Task UpdateStatusAsync(long id) + { + var banner = await GetValidBannerAsync(id); + banner.Status = banner.Status == (int)DefaultStatusEnum.Active + ? (int)DefaultStatusEnum.Inactive + : (int)DefaultStatusEnum.Active; + banner.UpdatedBy = "System"; + banner.UpdatedAt = DateTime.Now; + + var result = await bannerRepository.UpdateAsync(banner); + BusinessException.ThrowIf(!result, "更新轮播图状态失败", ResultCode.GLOBAL_ERROR); + + logger.LogInformation("轮播图状态更新成功:{Id},状态:{Status}", id, banner.Status); + return result; + } + + /// + /// 获取启用轮播图列表 + /// + public async Task> GetEnabledListAsync(BannerPositionEnum position) + { + var now = DateTime.Now; + var banners = await bannerRepository.Queryable() + .Where(b => !b.IsDeleted) + .Where(b => b.Status == (int)DefaultStatusEnum.Active) + .Where(b => b.Position == position) + .Where(b => b.StartTime == null || b.StartTime <= now) + .Where(b => b.EndTime == null || b.EndTime >= now) + .OrderBy(b => b.Sort) + .OrderByDescending(b => b.CreatedAt) + .ToListAsync(); + + return banners.Select(MapToOutput).ToList(); + } + + private async Task GetValidBannerAsync(long id) + { + var banner = await bannerRepository.Queryable() + .Where(b => b.Id == id && !b.IsDeleted) + .FirstAsync(); + + BusinessException.ThrowIf(banner == null, "轮播图不存在", ResultCode.NOT_FOUND); + return banner; + } + + private static void ValidateInput(BannerInput input) + { + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Title), "标题不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.Title.Trim().Length > 100, "标题长度不能超过100个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.ImageUrl), "轮播图不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.ImageUrl.Trim().Length > 500, "轮播图路径长度不能超过500个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.Sort < 0, "排序值不能小于0", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.StartTime.HasValue && input.EndTime.HasValue && input.EndTime <= input.StartTime, "结束时间必须大于开始时间", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.TargetType != BannerTargetTypeEnum.None && string.IsNullOrWhiteSpace(input.TargetValue), "跳转值不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.TargetValue) && input.TargetValue.Length > 500, "跳转值长度不能超过500个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.Remark) && input.Remark.Length > 500, "备注长度不能超过500个字符", ResultCode.BAD_REQUEST); + } + + private static string? NormalizeTargetValue(BannerInput input) + { + return input.TargetType == BannerTargetTypeEnum.None + ? null + : input.TargetValue?.Trim(); + } + + private OssImageHelper CreateImageHelper() => new(ossService, logger); + + private static string GetBannerFormalFolder(BannerPositionEnum position, long bannerId) + { + return $"banner/{position.ToString().ToLowerInvariant()}/{bannerId}"; + } + + private static BannerOutput MapToOutput(Banner banner) + { + return new BannerOutput + { + Id = banner.Id, + Title = banner.Title, + ImageUrl = banner.ImageUrl, + Position = banner.Position, + TargetType = banner.TargetType, + TargetValue = banner.TargetValue, + Sort = banner.Sort, + StartTime = banner.StartTime, + EndTime = banner.EndTime, + Remark = banner.Remark, + Status = banner.Status, + CreatedBy = banner.CreatedBy, + CreatedAt = banner.CreatedAt, + UpdatedBy = banner.UpdatedBy, + UpdatedAt = banner.UpdatedAt + }; + } +} diff --git a/QYZH.InteractiveMagazine.Service/MaterialService.cs b/QYZH.InteractiveMagazine.Service/MaterialService.cs new file mode 100644 index 0000000..42689a9 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/MaterialService.cs @@ -0,0 +1,334 @@ +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Infrastructure.OSS; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Material; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using SqlSugar; +using MaterialEntity = QYZH.InteractiveMagazine.Models.Entity.Material; +using MaterialFileEntity = QYZH.InteractiveMagazine.Models.Entity.MaterialFile; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 资料服务实现 +/// +public class MaterialService( + BaseRepository materialRepository, + BaseRepository materialFileRepository, + OssService ossService, + ILogger logger) : BaseRepository, IMaterialService +{ + /// + /// 创建资料 + /// + public async Task CreateAsync(MaterialInput input) + { + ValidateInput(input); + + var material = new MaterialEntity + { + Title = input.Title.Trim(), + Name = Normalize(input.Name), + Description = Normalize(input.Description), + Sort = input.Sort, + StartTime = input.StartTime, + EndTime = input.EndTime, + Status = (int)DefaultStatusEnum.Active, + IsDeleted = false, + CreatedBy = "System", + CreatedAt = DateTime.Now, + UpdatedBy = "System", + UpdatedAt = DateTime.Now + }; + + await materialRepository.UseTranAsync(async () => + { + var materialResult = await materialRepository.InsertAsync(material); + BusinessException.ThrowIf(!materialResult, "创建资料失败", ResultCode.GLOBAL_ERROR); + + var files = await BuildFileEntitiesAsync(material.Id, input.Files); + var fileResult = await materialFileRepository.Context.Insertable(files).ExecuteCommandAsync(); + BusinessException.ThrowIf(fileResult <= 0, "创建资料文件失败", ResultCode.GLOBAL_ERROR); + }); + + logger.LogInformation("资料创建成功:{Id}", material.Id); + return await GetByIdAsync(material.Id); + } + + /// + /// 更新资料 + /// + public async Task UpdateAsync(long id, MaterialInput input) + { + ValidateInput(input); + + var material = await GetValidMaterialAsync(id); + material.Title = input.Title.Trim(); + material.Name = Normalize(input.Name); + material.Description = Normalize(input.Description); + material.Sort = input.Sort; + material.StartTime = input.StartTime; + material.EndTime = input.EndTime; + material.UpdatedBy = "System"; + material.UpdatedAt = DateTime.Now; + + await materialRepository.UseTranAsync(async () => + { + var materialResult = await materialRepository.UpdateAsync(material); + BusinessException.ThrowIf(!materialResult, "更新资料失败", ResultCode.GLOBAL_ERROR); + + await SoftDeleteFilesAsync(material.Id); + var files = await BuildFileEntitiesAsync(material.Id, input.Files); + var fileResult = await materialFileRepository.Context.Insertable(files).ExecuteCommandAsync(); + BusinessException.ThrowIf(fileResult <= 0, "更新资料文件失败", ResultCode.GLOBAL_ERROR); + }); + + logger.LogInformation("资料更新成功:{Id}", material.Id); + return await GetByIdAsync(material.Id); + } + + /// + /// 删除资料 + /// + public async Task DeleteAsync(long id) + { + await GetValidMaterialAsync(id); + + await materialRepository.UseTranAsync(async () => + { + var materialResult = await materialRepository.Context.Updateable() + .SetColumns(m => new MaterialEntity + { + IsDeleted = true, + UpdatedBy = "System", + UpdatedAt = DateTime.Now + }) + .Where(m => m.Id == id && !m.IsDeleted) + .ExecuteCommandAsync(); + BusinessException.ThrowIf(materialResult <= 0, "删除资料失败", ResultCode.GLOBAL_ERROR); + + await SoftDeleteFilesAsync(id); + }); + + logger.LogInformation("资料删除成功:{Id}", id); + } + + /// + /// 根据ID获取资料 + /// + public async Task GetByIdAsync(long id) + { + var material = await GetValidMaterialAsync(id); + var files = await GetFilesAsync(material.Id); + return MapToOutput(material, files); + } + + /// + /// 分页查询资料列表 + /// + public async Task> GetListAsync(MaterialQueryInput input) + { + BusinessException.ThrowIf(input.PageIndex <= 0, "页码必须大于0", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.PageSize <= 0 || input.PageSize > 100, "每页条数必须在1-100之间", ResultCode.BAD_REQUEST); + + RefAsync totalNumber = 0; + var materials = await materialRepository.Queryable() + .Where(m => !m.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), m => m.Title.Contains(input.Title!.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), m => m.Name != null && m.Name.Contains(input.Name!.Trim())) + .WhereIF(input.Status.HasValue, m => m.Status == input.Status!.Value) + .OrderBy(m => m.Sort) + .OrderByDescending(m => m.CreatedAt) + .ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); + + var result = await BuildOutputsAsync(materials); + return new PageListModel(result, input.PageIndex, input.PageSize, totalNumber); + } + + /// + /// 更新资料启用/禁用状态 + /// + public async Task UpdateStatusAsync(long id) + { + var material = await GetValidMaterialAsync(id); + material.Status = material.Status == (int)DefaultStatusEnum.Active + ? (int)DefaultStatusEnum.Inactive + : (int)DefaultStatusEnum.Active; + material.UpdatedBy = "System"; + material.UpdatedAt = DateTime.Now; + + var result = await materialRepository.UpdateAsync(material); + BusinessException.ThrowIf(!result, "更新资料状态失败", ResultCode.GLOBAL_ERROR); + + logger.LogInformation("资料状态更新成功:{Id},状态:{Status}", id, material.Status); + return result; + } + + /// + /// 获取小程序可见资料列表 + /// + public async Task> GetEnabledListAsync() + { + var now = DateTime.Now; + var materials = await materialRepository.Queryable() + .Where(m => !m.IsDeleted) + .Where(m => m.Status == (int)DefaultStatusEnum.Active) + .Where(m => m.StartTime == null || m.StartTime <= now) + .Where(m => m.EndTime == null || m.EndTime >= now) + .OrderBy(m => m.Sort) + .OrderByDescending(m => m.CreatedAt) + .ToListAsync(); + + return await BuildOutputsAsync(materials); + } + + private async Task GetValidMaterialAsync(long id) + { + var material = await materialRepository.Queryable() + .Where(m => m.Id == id && !m.IsDeleted) + .FirstAsync(); + + BusinessException.ThrowIf(material == null, "资料不存在", ResultCode.NOT_FOUND); + return material; + } + + private async Task> BuildFileEntitiesAsync(long materialId, List inputs) + { + var helper = CreateImageHelper(); + var files = new List(); + + foreach (var input in inputs.OrderBy(f => f.Sort)) + { + var file = new MaterialFileEntity + { + MaterialId = materialId, + FileType = input.FileType, + FileUrl = input.FileUrl.Trim(), + FileName = Normalize(input.FileName), + Sort = input.Sort, + Status = (int)DefaultStatusEnum.Active, + IsDeleted = false, + CreatedBy = "System", + CreatedAt = DateTime.Now, + UpdatedBy = "System", + UpdatedAt = DateTime.Now + }; + + if (OssImageHelper.IsTempImage(file.FileUrl)) + { + file.FileUrl = await helper.MoveToFormalAsync(file.FileUrl, GetMaterialFormalFolder(materialId, file.FileType, file.Id)); + } + + files.Add(file); + } + + return files; + } + + private async Task SoftDeleteFilesAsync(long materialId) + { + await materialFileRepository.Context.Updateable() + .SetColumns(f => new MaterialFileEntity + { + IsDeleted = true, + UpdatedBy = "System", + UpdatedAt = DateTime.Now + }) + .Where(f => f.MaterialId == materialId && !f.IsDeleted) + .ExecuteCommandAsync(); + } + + private async Task> GetFilesAsync(long materialId) + { + return await materialFileRepository.Queryable() + .Where(f => f.MaterialId == materialId && !f.IsDeleted) + .OrderBy(f => f.Sort) + .OrderByDescending(f => f.CreatedAt) + .ToListAsync(); + } + + private async Task> BuildOutputsAsync(List materials) + { + if (materials.Count == 0) + { + return new List(); + } + + var materialIds = materials.Select(m => m.Id).ToList(); + var files = await materialFileRepository.Queryable() + .Where(f => materialIds.Contains(f.MaterialId) && !f.IsDeleted) + .OrderBy(f => f.Sort) + .OrderByDescending(f => f.CreatedAt) + .ToListAsync(); + var fileMap = files.GroupBy(f => f.MaterialId).ToDictionary(g => g.Key, g => g.ToList()); + + return materials.Select(m => MapToOutput(m, fileMap.GetValueOrDefault(m.Id) ?? new List())).ToList(); + } + + private static void ValidateInput(MaterialInput input) + { + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Title), "资料标题不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.Title.Trim().Length > 100, "资料标题长度不能超过100个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.Name) && input.Name.Length > 100, "资料名称长度不能超过100个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.Description) && input.Description.Length > 1000, "资料描述长度不能超过1000个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.Sort < 0, "排序值不能小于0", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.StartTime.HasValue && input.EndTime.HasValue && input.EndTime <= input.StartTime, "结束时间必须大于开始时间", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(input.Files == null || input.Files.Count == 0, "资料文件不能为空", ResultCode.BAD_REQUEST); + + foreach (var file in input.Files) + { + BusinessException.ThrowIf(!Enum.IsDefined(typeof(MaterialFileTypeEnum), file.FileType), "资料文件类型不正确", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(file.FileUrl), "资料文件地址不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(file.FileUrl.Trim().Length > 500, "资料文件地址长度不能超过500个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(file.FileName) && file.FileName.Length > 100, "资料文件名称长度不能超过100个字符", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(file.Sort < 0, "资料文件排序值不能小于0", ResultCode.BAD_REQUEST); + } + } + + private OssImageHelper CreateImageHelper() => new(ossService, logger); + + private static string GetMaterialFormalFolder(long materialId, MaterialFileTypeEnum fileType, long fileId) + { + return $"material/{materialId}/{fileType.ToString().ToLowerInvariant()}/{fileId}"; + } + + private static string? Normalize(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + private static MaterialOutput MapToOutput(MaterialEntity material, List files) + { + return new MaterialOutput + { + Id = material.Id, + Title = material.Title, + Name = material.Name, + Description = material.Description, + Sort = material.Sort, + StartTime = material.StartTime, + EndTime = material.EndTime, + Status = material.Status, + CreatedBy = material.CreatedBy, + CreatedAt = material.CreatedAt, + UpdatedBy = material.UpdatedBy, + UpdatedAt = material.UpdatedAt, + Files = files.Select(MapFileToOutput).ToList() + }; + } + + private static MaterialFileOutput MapFileToOutput(MaterialFileEntity file) + { + return new MaterialFileOutput + { + Id = file.Id, + FileType = file.FileType, + FileUrl = file.FileUrl, + FileName = file.FileName, + Sort = file.Sort + }; + } +} diff --git a/QYZH.InteractiveMagazine.WeChatApi/Controllers/BannerController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/BannerController.cs new file mode 100644 index 0000000..d661469 --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/BannerController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Banner; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; + +/// +/// 小程序轮播图控制器 +/// +public class BannerController(IBannerService bannerService) : WeChatBaseController +{ + /// + /// 获取启用轮播图列表 + /// + /// 展示位置 + /// 轮播图列表 + [HttpGet] + public async Task>> GetListAsync([FromQuery] BannerPositionEnum position = BannerPositionEnum.Home) + { + var result = await bannerService.GetEnabledListAsync(position); + return Success(result); + } +} diff --git a/QYZH.InteractiveMagazine.WeChatApi/Controllers/MaterialController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/MaterialController.cs new file mode 100644 index 0000000..09eff12 --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/MaterialController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Material; + +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; + +/// +/// 小程序资料控制器 +/// +public class MaterialController(IMaterialService materialService) : WeChatBaseController +{ + /// + /// 获取可见资料列表 + /// + /// 资料列表 + [HttpGet] + public async Task>> GetListAsync() + { + var result = await materialService.GetEnabledListAsync(); + return Success(result); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs new file mode 100644 index 0000000..4d767fa --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Banner; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 轮播图管理控制器 +/// +[Route("api/[controller]")] +[ApiController] +[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] +public class BannerController(IBannerService bannerService) : BaseController +{ + /// + /// 创建轮播图 + /// + /// 轮播图信息 + /// 创建后的轮播图信息 + [HttpPost] + public async Task> CreateAsync([FromBody] BannerInput input) + { + var result = await bannerService.CreateAsync(input); + return Success(result, "创建轮播图成功"); + } + + /// + /// 更新轮播图 + /// + /// 轮播图ID + /// 轮播图信息 + /// 更新后的轮播图信息 + [HttpPut("{id}")] + public async Task> UpdateAsync(long id, [FromBody] BannerInput input) + { + var result = await bannerService.UpdateAsync(id, input); + return Success(result, "更新轮播图成功"); + } + + /// + /// 删除轮播图 + /// + /// 轮播图ID + /// 操作结果 + [HttpDelete("{id}")] + public async Task> DeleteAsync(long id) + { + await bannerService.DeleteAsync(id); + return Success(new object(), "删除轮播图成功"); + } + + /// + /// 根据ID获取轮播图 + /// + /// 轮播图ID + /// 轮播图信息 + [HttpGet("{id}")] + public async Task> GetByIdAsync(long id) + { + var result = await bannerService.GetByIdAsync(id); + return Success(result); + } + + /// + /// 分页查询轮播图列表 + /// + /// 查询条件 + /// 分页结果 + [HttpPost("list")] + public async Task>> GetListAsync([FromBody] BannerQueryInput input) + { + var result = await bannerService.GetListAsync(input); + return Success(result); + } + + /// + /// 更新轮播图启用/禁用状态 + /// + /// 轮播图ID + /// 操作结果 + [HttpPut("{id}/status")] + public async Task> UpdateStatusAsync(long id) + { + var result = await bannerService.UpdateStatusAsync(id); + return Success(result, "更新轮播图状态成功"); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs new file mode 100644 index 0000000..ef2a38e --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.Material; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 资料管理控制器 +/// +[Route("api/[controller]")] +[ApiController] +[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] +public class MaterialController(IMaterialService materialService) : BaseController +{ + /// + /// 创建资料 + /// + /// 资料信息 + /// 创建后的资料信息 + [HttpPost] + public async Task> CreateAsync([FromBody] MaterialInput input) + { + var result = await materialService.CreateAsync(input); + return Success(result, "创建资料成功"); + } + + /// + /// 更新资料 + /// + /// 资料ID + /// 资料信息 + /// 更新后的资料信息 + [HttpPut("{id}")] + public async Task> UpdateAsync(long id, [FromBody] MaterialInput input) + { + var result = await materialService.UpdateAsync(id, input); + return Success(result, "更新资料成功"); + } + + /// + /// 删除资料 + /// + /// 资料ID + /// 操作结果 + [HttpDelete("{id}")] + public async Task> DeleteAsync(long id) + { + await materialService.DeleteAsync(id); + return Success(new object(), "删除资料成功"); + } + + /// + /// 根据ID获取资料 + /// + /// 资料ID + /// 资料信息 + [HttpGet("{id}")] + public async Task> GetByIdAsync(long id) + { + var result = await materialService.GetByIdAsync(id); + return Success(result); + } + + /// + /// 分页查询资料列表 + /// + /// 查询条件 + /// 分页结果 + [HttpPost("list")] + public async Task>> GetListAsync([FromBody] MaterialQueryInput input) + { + var result = await materialService.GetListAsync(input); + return Success(result); + } + + /// + /// 更新资料启用/禁用状态 + /// + /// 资料ID + /// 操作结果 + [HttpPut("{id}/status")] + public async Task> UpdateStatusAsync(long id) + { + var result = await materialService.UpdateStatusAsync(id); + return Success(result, "更新资料状态成功"); + } +}