feat: 新增轮播图和资料管理模块

此提交完整实现了小程序端和管理后台的轮播图、资料管理功能:
1. 新增3个枚举类型:轮播图展示位置、跳转类型、资料文件类型
2. 新增数据库表结构SQL脚本,包含Banner、Material、MaterialFile表
3. 新增实体类定义,适配SqlSugar ORM
4. 新增服务接口与实现,包含CRUD、状态切换、列表查询等完整业务逻辑
5. 新增管理后台API控制器和小程序端API控制器
6. 新增数据传输对象,包含输入、输出、查询参数等类型
7. 实现了OSS临时文件迁移、参数校验、分页查询、软删除等基础功能
This commit is contained in:
glz
2026-07-07 15:53:25 +08:00
parent 0b829b72cb
commit 24059dfebb
16 changed files with 1534 additions and 0 deletions

View File

@ -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;
/// <summary>
/// 轮播图服务实现
/// </summary>
public class BannerService(
BaseRepository<Banner> bannerRepository,
OssService ossService,
ILogger<BannerService> logger) : BaseRepository<Banner>, IBannerService
{
/// <summary>
/// 创建轮播图
/// </summary>
public async Task<BannerOutput> 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);
}
/// <summary>
/// 更新轮播图
/// </summary>
public async Task<BannerOutput> 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);
}
/// <summary>
/// 删除轮播图
/// </summary>
public async Task DeleteAsync(long id)
{
await GetValidBannerAsync(id);
var result = await bannerRepository.Context.Updateable<Banner>()
.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);
}
/// <summary>
/// 根据ID获取轮播图
/// </summary>
public async Task<BannerOutput> GetByIdAsync(long id)
{
var banner = await GetValidBannerAsync(id);
return MapToOutput(banner);
}
/// <summary>
/// 分页查询轮播图列表
/// </summary>
public async Task<PageListModel<BannerOutput>> 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<int> 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<BannerOutput>(result, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 更新轮播图启用/禁用状态
/// </summary>
public async Task<bool> 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;
}
/// <summary>
/// 获取启用轮播图列表
/// </summary>
public async Task<List<BannerOutput>> 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<Banner> 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
};
}
}