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 }; } }