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, "更新资料状态成功");
+ }
+}