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

335 lines
13 KiB
C#

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;
/// <summary>
/// 资料服务实现
/// </summary>
public class MaterialService(
BaseRepository<MaterialEntity> materialRepository,
BaseRepository<MaterialFileEntity> materialFileRepository,
OssService ossService,
ILogger<MaterialService> logger) : BaseRepository<MaterialEntity>, IMaterialService
{
/// <summary>
/// 创建资料
/// </summary>
public async Task<MaterialOutput> 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);
}
/// <summary>
/// 更新资料
/// </summary>
public async Task<MaterialOutput> 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);
}
/// <summary>
/// 删除资料
/// </summary>
public async Task DeleteAsync(long id)
{
await GetValidMaterialAsync(id);
await materialRepository.UseTranAsync(async () =>
{
var materialResult = await materialRepository.Context.Updateable<MaterialEntity>()
.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);
}
/// <summary>
/// 根据ID获取资料
/// </summary>
public async Task<MaterialOutput> GetByIdAsync(long id)
{
var material = await GetValidMaterialAsync(id);
var files = await GetFilesAsync(material.Id);
return MapToOutput(material, files);
}
/// <summary>
/// 分页查询资料列表
/// </summary>
public async Task<PageListModel<MaterialOutput>> 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<int> 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<MaterialOutput>(result, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 更新资料启用/禁用状态
/// </summary>
public async Task<bool> 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;
}
/// <summary>
/// 获取小程序可见资料列表
/// </summary>
public async Task<List<MaterialOutput>> 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<MaterialEntity> 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<List<MaterialFileEntity>> BuildFileEntitiesAsync(long materialId, List<MaterialFileInput> inputs)
{
var helper = CreateImageHelper();
var files = new List<MaterialFileEntity>();
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<MaterialFileEntity>()
.SetColumns(f => new MaterialFileEntity
{
IsDeleted = true,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
})
.Where(f => f.MaterialId == materialId && !f.IsDeleted)
.ExecuteCommandAsync();
}
private async Task<List<MaterialFileEntity>> 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<List<MaterialOutput>> BuildOutputsAsync(List<MaterialEntity> materials)
{
if (materials.Count == 0)
{
return new List<MaterialOutput>();
}
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<MaterialFileEntity>())).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<MaterialFileEntity> 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
};
}
}