using Mapster; using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.Common.Extensions; using QYZH.InteractiveMagazine.Common.Helpers; using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.DotMatrix; using QYZH.InteractiveMagazine.Models.Dto.Journal; using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Repository; using SqlSugar; using System.Text.Json; using Yitter.IdGenerator; namespace QYZH.InteractiveMagazine.Service; public class JournalService(BaseRepository JournalPageRepository, BaseRepository JournalPageTaskRepository, BaseRepository JournalPageTaskAnswerRepository, BaseRepository JournalCatalogRepository, BaseRepository dotFileRepository, BaseRepository dotFileDetailRepository, OssService ossService, IMessagePublishService messagePublishService, ILogger logger) : BaseRepository, IJournalService { private const string JournalExchange = "ex.journal"; private const string PublishBookQueue = "mq.journal.publish.book"; private const string PublishBookRoutingKey = "rk.journal.publish.book"; private const string PublishBookPageQueue = "mq.journal.publish.bookpage"; private const string PublishBookPageRoutingKey = "rk.journal.publish.bookpage"; /// /// 查询List /// /// /// public async Task> GetListAsync(JournalQueryDto dto) { var query = await Queryable() .WhereIF(!string.IsNullOrWhiteSpace(dto.Name), a => a.Name.Contains(dto.Name)) .WhereIF(dto.Status.HasValue, a => a.Status == (int)dto.Status) .Select(a => new JournalDto(), true) .ToListAsync(); return query; } /// /// 分页查询 /// /// /// public async Task> GetPageListAsync(PageQueryModel search) { RefAsync totalNumber = 0; var dataList = await Queryable() //.InnerJoin((a, b) => a.Id == b.JournalId) .WhereIF(!string.IsNullOrWhiteSpace(search.Params.Name), a => a.Name.Contains(search.Params.Name)) .WhereIF(search.Params.Status.HasValue, a => a.Status == (int)search.Params.Status) .WhereIF(!string.IsNullOrWhiteSpace(search.Params.Name), a => a.Name.Contains(search.Params.Name)) .OrderByDescending(a => a.CreatedAt) .Select(a => new JournalDto(), true) .ToPageListAsync(search.PageIndex, search.PageSize, totalNumber); return new PageListModel(dataList, search.PageIndex, search.PageSize, totalNumber); } /// /// 创建杂志 /// /// 杂志信息 /// 新杂志ID public async Task> AddAsync(JournalAddDto input) { BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Name), "书籍名称不能为空", ResultCode.BAD_REQUEST); var journal = new Journal { Name = input.Name.Trim(), Title = input.Title, TotalPage = input.TotalPage, Status = (int)JournalStatusEnum.Created, PdfUrl = input.PdfUrl?.RemoveDomain(), Width = input.Width, Height = input.Height, Cover = input.Cover?.RemoveDomain(), BackCover = input.BackCover?.RemoveDomain(), PdfPreviewUrl = input.PdfPreviewUrl?.RemoveDomain(), StartTime = input.StartTime, EndTime = input.EndTime, CreatedBy = "System", UpdatedBy = "System", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, IsDeleted = false }; // Id 已由 YitIdHelper 预生成,先搬运文件到正式目录,再一次性插入 if (!string.IsNullOrWhiteSpace(input.Cover)) { var coverKey = $"journal/{journal.Id}/cover.{input.Cover.ToExtension()}"; ossService.CopyObject(input.Cover.RemoveDomain(), coverKey); journal.Cover = coverKey; } if (!string.IsNullOrWhiteSpace(input.BackCover)) { var backCoverKey = $"journal/{journal.Id}/backCover.{input.BackCover.ToExtension()}"; ossService.CopyObject(input.BackCover.RemoveDomain(), backCoverKey); journal.BackCover = backCoverKey; } if (!string.IsNullOrWhiteSpace(input.PdfUrl)) { var pdfKey = $"journal/{journal.Id}/Journal.{input.PdfUrl.ToExtension()}"; ossService.CopyObject(input.PdfUrl.RemoveDomain(), pdfKey); journal.PdfUrl = pdfKey; } var res = await base.InsertAsync(journal); if (!res) { new BusinessException("创建失败", ResultCode.GLOBAL_ERROR); } logger.LogInformation("杂志创建成功,ID: {Id}, 名称: {Name}", journal.Id, input.Name); return BaseResponse.Success(journal.Id); } /// /// 编辑 /// /// /// public async Task> EditAsync(JournalEditDto input) { var Journal = await base.GetByIdAsync(input.Id); BusinessException.ThrowIf(Journal.IsNull(), "不存在的Id", ResultCode.NOT_FOUND); BusinessException.ThrowIf(Journal.Status == (int)JournalStatusEnum.Archive, "已归档不可编辑", ResultCode.CONFLICT); if (string.IsNullOrWhiteSpace(input.Cover)) { Journal.Cover = null; } else if (input.Cover.RemoveDomain() != Journal.Cover) { var key = $"journal/{Journal.Id}/cover.{input.Cover.ToExtension()}"; ossService.CopyObject(input.Cover.RemoveDomain(), key); Journal.Cover = key; } if (string.IsNullOrWhiteSpace(input.BackCover)) { Journal.BackCover = null; } else if (input.BackCover.RemoveDomain() != Journal.BackCover) { var backCoverkey = $"journal/{Journal.Id}/backCover.{input.BackCover.ToExtension()}"; ossService.CopyObject(input.BackCover.RemoveDomain(), backCoverkey); Journal.BackCover = backCoverkey; } if (string.IsNullOrWhiteSpace(input.PdfUrl)) { Journal.PdfUrl = null; } else if (input.PdfUrl.RemoveDomain() != Journal.PdfUrl) { var pdfUrlkey = $"journal/{Journal.Id}/Journal.{input.PdfUrl.ToExtension()}"; ossService.CopyObject(input.PdfUrl.RemoveDomain(), pdfUrlkey); Journal.PdfUrl = pdfUrlkey; } Journal.Width = input.Width; Journal.Height = input.Height; Journal.Name = input.Name; Journal.Title = input.Title; Journal.StartTime = input.StartTime; Journal.EndTime = input.EndTime; Journal.UpdatedAt = DateTime.Now; var res = await UseTranAsync(async () => { var result = await Context.Updateable(Journal).IgnoreColumns(c => c.Status).ExecuteCommandAsync(); return result > 0; }); return BaseResponse.Success(res); } public async Task> Tasks(long id) { var data = await JournalPageTaskRepository.Queryable().Where(w => w.JournalId == id) .Select(x => new JournalTaskOutput() { TaskId = x.Id, TaskNo = x.No, TaskSubType = x.Type }).ToListAsync(); return data.OrderBy(x => { var parts = x.TaskNo.Split('-').Select(int.Parse).ToArray(); return (parts[0], parts[1], parts[2], parts[3]); // 元组 }).ToList(); } public async Task DetailAsync(long id) { var Journal = await base.Queryable().Where(w => w.Id == id).Select().FirstAsync(); return Journal; } public async Task DeleteAsync(List ids) { BusinessException.ThrowIf(!base.Queryable().Any(w => ids.Contains(w.Id)), "ID不存在", ResultCode.NOT_FOUND); BusinessException.ThrowIf(base.Queryable().Any(w => ids.Contains(w.Id) && w.Status == (int)JournalStatusEnum.Archive), "已归档不可删除", ResultCode.CONFLICT); var result = await UseTranAsync(async () => { await base.DeleteAsync(d => ids.Contains(d.Id)); await JournalPageRepository.DeleteAsync(d => ids.Contains(d.JournalId)); await JournalCatalogRepository.DeleteAsync(d => ids.Contains(d.JournalId)); // 先查询要删除的 JournalPageTask Id 列表 var taskIds = await JournalPageTaskRepository.Queryable() .Where(d => ids.Contains(d.JournalId)) .Select(d => d.Id) .ToListAsync(); // 删除 JournalPageTaskAnswer if (taskIds.Any()) { await JournalPageTaskAnswerRepository.DeleteAsync(d => taskIds.Contains(d.JournalPageTaskId)); } // 再删除 JournalPageTask await JournalPageTaskRepository.DeleteAsync(d => ids.Contains(d.JournalId)); return true; }); //删除OSS上的journal/{JournalId}文件夹 if (result) { foreach (var JournalId in ids) { var prefix = $"journal/{JournalId}/"; var keys = ossService.ListObjects(prefix); if (keys?.Count > 0) { ossService.DeleteObjects(keys); } } } return result; } /// /// /// /// 书id /// 起始页码 /// public async Task StartPageAsync(long id, int index) { var oldIndex = await JournalPageRepository.Queryable().Where(w => w.JournalId == id && w.PageNum == 1).Select(x => x.Sort).FirstAsync(); // 如果位置没有变化,直接返回 if (index == oldIndex) return true; if (index > oldIndex) { // 向后移动的情况 var x = index - oldIndex; await JournalPageRepository.Updateable().SetColumns(s => s.PageNum, 0).Where(w => w.Sort < index).Where(w => w.JournalId == id).ExecuteCommandAsync(); await JournalPageRepository.Updateable().SetColumns(s => s.PageNum == s.PageNum - x).Where(w => w.Sort >= index).Where(w => w.JournalId == id).ExecuteCommandAsync(); } else { // 向前移动的情况 //var x = oldIndex - input.Index; await JournalPageRepository.Updateable().SetColumns(s => s.PageNum, 0).Where(w => w.Sort < index).Where(w => w.JournalId == id).ExecuteCommandAsync(); // 将目标页面设置为第一页 await JournalPageRepository.Updateable().SetColumns(s => s.PageNum == s.Sort - (index - 1)).Where(w => w.Sort >= index).Where(w => w.JournalId == id).ExecuteCommandAsync(); } // await JournalPageRepository.Updateable().SetColumns(s => s.PageNum == s.Sort).Where(w => w.JournalId == input.Id).ExecuteCommandAsync(); return true; } public async Task PrintCodeAsync(long id) { var Journal = await base.Queryable().Where(w => w.Id == id).FirstAsync(); BusinessException.ThrowIf(Journal.IsNull(), "不存在书", ResultCode.NOT_FOUND); BusinessException.ThrowIf(Journal.Status == (int)JournalStatusEnum.Codeing, "正在生成中...", ResultCode.CONFLICT); var pages = await JournalPageRepository.Queryable().Where(w => w.JournalId == id).ToListAsync(); BusinessException.ThrowIf(pages.IsNull(), "不存在页", ResultCode.NOT_FOUND); var output = new DotMatrixOutput() { Method = "Journal", Name = Journal.Name, NoteId = id, PdfUrl = Journal.PdfUrl }; return output; } public async Task ResultReportAsync(DotMatrixNoteJournalReportInput input) { if (input.FileUrl.IsNull() && input.FileKey.IsNull()) { return await Updateable().SetColumns(s => s.Status, input.Success ? JournalStatusEnum.CodeSuccess : JournalStatusEnum.CodeFail) .SetColumns(s => s.PdfPreviewUrl, input.FileKey) .Where(w => w.Id == input.Id) .ExecuteCommandAsync() > 0; } return await Updateable().SetColumns(s => s.Status, JournalStatusEnum.CodeFail).Where(w => w.Id == input.Id).ExecuteCommandAsync() > 0; } public async Task StatusAsync(long id, JournalStatusEnum status) { var book = await base.GetByIdAsync(id); var tasks = await Context.Queryable().Where(w => w.JournalId == id).ToListAsync(); BusinessException.ThrowIf(tasks.Count == 0 && status == JournalStatusEnum.Archive, "未添加任何题目,无法归档", ResultCode.UNPROCESSABLE_ENTITY); BusinessException.ThrowIf(tasks.Any(a => string.IsNullOrWhiteSpace(a.TaskUrl)) && status == JournalStatusEnum.Archive, $"{string.Join(',', tasks.Where(a => string.IsNullOrWhiteSpace(a.TaskUrl)).Select(a => a.No).ToList())}未保存,无法归档", ResultCode.UNPROCESSABLE_ENTITY); var res = await base.Updateable().SetColumns(s => s.Status, status).Where(w => w.Id == id).ExecuteCommandAsync() > 0; return res; } public async Task PublishAsync(long id) { var book = await base.GetByIdAsync(id); BusinessException.ThrowIf(book == null || book.IsDeleted, "杂志不存在", ResultCode.NOT_FOUND); var pages = await JournalPageRepository.Queryable() .Where(x => x.JournalId == id && !x.IsDeleted) .OrderBy(x => x.PageNum) .OrderBy(x => x.Sort) .Select(x => new { x.Id, x.PageNo, x.Layout, x.Url }) .ToListAsync(); BusinessException.ThrowIf(pages.Count == 0, "书籍未添加任何书页,无法发布", ResultCode.UNPROCESSABLE_ENTITY); BusinessException.ThrowIf(!book.StartTime.HasValue, "发布开始时间不能为空", ResultCode.UNPROCESSABLE_ENTITY); BusinessException.ThrowIf(!book.EndTime.HasValue, "发布结束时间不能为空", ResultCode.UNPROCESSABLE_ENTITY); BusinessException.ThrowIf(book.EndTime < book.StartTime, "发布结束时间不能早于开始时间", ResultCode.UNPROCESSABLE_ENTITY); var pageIds = pages.Select(x => x.Id).ToList(); var pageQuestions = await JournalPageTaskRepository.Queryable() .Where(x => x.JournalId == id && pageIds.Contains(x.JournalPageId) && !x.IsDeleted) .Select(x => new { x.JournalPageId, x.Id, x.No }) .ToListAsync(); var questionIdsByPageId = pageQuestions .GroupBy(x => x.JournalPageId) .ToDictionary( x => x.Key, x => string.Join(',', x.OrderBy(q => ParseTaskNo(q.No)).Select(q => q.Id))); var publishMessages = new List> { new() { Exchange = JournalExchange, Queue = PublishBookQueue, RoutingKey = PublishBookRoutingKey, Data = new JournalPublishBookMessage { BookId = id, StartTime = book.StartTime.Value, EndTime = book.EndTime.Value }, BusinessType = "JournalPublish", BusinessId = id } }; foreach (var page in pages) { publishMessages.Add(new MessagePublishInput { Exchange = JournalExchange, Queue = PublishBookPageQueue, RoutingKey = PublishBookPageRoutingKey, Data = new JournalPublishBookPageMessage { BookId = id, PageId = page.Id, PageNo = page.PageNo, Layout = page.Layout, Url = DomainHelper.OssFullUrl(page.Url), QuestionNo = questionIdsByPageId.GetValueOrDefault(page.Id) ?? string.Empty }, BusinessType = "JournalPublish", BusinessId = id }); } return await UseTranAsync(async () => { await messagePublishService.PublishBatchAsync(publishMessages); return await base.Updateable() .SetColumns(s => s.Status, JournalStatusEnum.Published) .SetColumns(s => s.UpdatedAt, DateTime.Now) .Where(w => w.Id == id) .ExecuteCommandAsync() > 0; }); static (int First, int Second, int Third, int Fourth) ParseTaskNo(string? no) { var parts = no?.Split('-') ?? Array.Empty(); return (GetNoPart(parts, 0), GetNoPart(parts, 1), GetNoPart(parts, 2), GetNoPart(parts, 3)); } static int GetNoPart(string[] parts, int index) { return parts.Length > index && int.TryParse(parts[index], out var value) ? value : int.MaxValue; } } }