From e493c85d08df335efdf1dea8d2f69f2d4cb300aa Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Thu, 11 Jun 2026 17:01:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor,feat:=20=E6=89=B9=E9=87=8F=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=87=8D=E6=9E=84=E4=B8=8E=E6=96=B0=E5=A2=9E=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重命名枚举项与实体字段,修正类型引用 2. 新增IsNull扩展方法与多项字符串处理扩展 3. 新增大量业务DTO、服务接口与枚举定义 4. 重构RabbitMQ服务实现,替换旧版消息队列组件 5. 优化签到服务的宠物喂养事务逻辑 6. 移除冗余的项目引用与旧版消息队列代码 7. 新增Excel导出、导入模板相关工具方法 --- .../Extensions/Extension.Convert.cs | 441 ++++++++++++++ .../Extensions/Extension.Validate.cs | 44 ++ .../Extensions/ObjectExtension.cs | 5 + .../Extensions/StringExtension.cs | 50 ++ .../QYZH.InteractiveMagazine.Common.csproj | 4 - .../IJournalCatalogService.cs | 45 ++ .../IJournalPageService.cs | 32 + .../IJournalPageTaskService.cs | 17 + .../IJournalService.cs | 48 ++ .../IPetService.cs | 8 + .../Autofac/AutofacExtension.cs | 25 - .../MessageQueue/RabbitMQConsumer.cs | 72 --- .../MessageQueue/RabbitMQPublisher.cs | 47 -- .../OSS/OssService.cs | 4 +- .../RabbitMQ/IRabbitMQService.cs | 14 + .../RabbitMQ/RabbitMQConnection.cs | 62 ++ .../RabbitMQ/RabbitMQOptions.cs | 13 + .../RabbitMQ/RabbitMQSendParam.cs | 25 + .../RabbitMQ/RabbitMQService.cs | 157 +++++ .../RabbitMQ/RabbiteMQExtensions.cs | 56 ++ .../Common/BusinessException.cs | 19 + .../DotMatrix/DotMatrixNotebookReportInput.cs | 19 + .../Dto/DotMatrix/DotMatrixOutput.cs | 73 +++ .../Dto/Journal/BindJournalDto.cs | 81 +++ .../Dto/Journal/JournalCatalogAddInput.cs | 112 ++++ .../Dto/Journal/JournalCatalogTreeListDto.cs | 71 +++ .../Dto/Journal/JournalCatalogUpdateInput.cs | 15 + .../Dto/Journal/JournalDto.cs | 214 ++++--- .../Dto/Journal/JournalEditDto.cs | 116 ++++ .../Dto/Journal/JournalPageAddV2Input.cs | 28 + .../Dto/Journal/JournalPageTaskAddInput.cs | 62 ++ .../Journal/JournalPageTaskKeywordAnalysis.cs | 21 + .../Dto/Journal/JournalPageTaskOutput.cs | 158 +++++ .../Dto/Journal/JournalPageTaskUpdateInput.cs | 126 ++++ .../Dto/Journal/JournalPageV2Output.cs | 100 ++++ .../Dto/Journal/JournalQueryDto.cs | 22 + .../Dto/Journal/JournalQuestionOutput.cs | 35 ++ .../Dto/Journal/PageLayoutInput.cs | 21 + .../Dto/MoveInput.cs | 29 + .../Entity/Journal.cs | 146 +++-- .../Entity/JournalPageTask.cs | 2 +- .../Enum/JournalPageTaskTypeEnum.cs | 2 +- .../Enum/JournalStatusEnum.cs | 52 +- .../Enum/TaskBankTypeEnum.cs | 77 +++ .../Enum/TaskTypeEnum.cs | 24 + .../QYZH.InteractiveMagazine.Models.csproj | 5 + .../CheckInService.cs | 84 ++- .../JournalCatalogService.cs | 526 ++++++++++++++++ .../JournalPageService.cs | 211 +++++++ .../JournalPageTaskService.cs | 176 ++++++ .../JournalService.cs | 313 ++++++++++ .../PetService.cs | 140 +++-- .../UsersService.cs | 2 +- .../Controllers/BaseController.cs | 57 ++ .../Controllers/JournalController.cs | 559 ++++++++++++++++++ QYZH.InteractiveMagazine.WebApi/Program.cs | 4 + .../appsettings.json | 5 +- 57 files changed, 4477 insertions(+), 399 deletions(-) create mode 100644 QYZH.InteractiveMagazine.Common/Extensions/Extension.Convert.cs create mode 100644 QYZH.InteractiveMagazine.Common/Extensions/Extension.Validate.cs create mode 100644 QYZH.InteractiveMagazine.IService/IJournalCatalogService.cs create mode 100644 QYZH.InteractiveMagazine.IService/IJournalPageService.cs create mode 100644 QYZH.InteractiveMagazine.IService/IJournalPageTaskService.cs create mode 100644 QYZH.InteractiveMagazine.IService/IJournalService.cs delete mode 100644 QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs delete mode 100644 QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQConnection.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQOptions.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQSendParam.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbiteMQExtensions.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixNotebookReportInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogAddInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogTreeListDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogUpdateInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalEditDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageAddV2Input.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskKeywordAnalysis.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageV2Output.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQueryDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQuestionOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Journal/PageLayoutInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/MoveInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Enum/TaskBankTypeEnum.cs create mode 100644 QYZH.InteractiveMagazine.Models/Enum/TaskTypeEnum.cs create mode 100644 QYZH.InteractiveMagazine.Service/JournalCatalogService.cs create mode 100644 QYZH.InteractiveMagazine.Service/JournalPageService.cs create mode 100644 QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs create mode 100644 QYZH.InteractiveMagazine.Service/JournalService.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs diff --git a/QYZH.InteractiveMagazine.Common/Extensions/Extension.Convert.cs b/QYZH.InteractiveMagazine.Common/Extensions/Extension.Convert.cs new file mode 100644 index 0000000..2544edf --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Extensions/Extension.Convert.cs @@ -0,0 +1,441 @@ +using System.Collections; + +namespace QYZH.InteractiveMagazine.Common.Extensions +{ + public static partial class Extensions + { + #region 转换为long + /// + /// 将object转换为long,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static long ParseToLong(this object obj) + { + try + { + return long.Parse(obj.ToString()); + } + catch + { + return 0L; + } + } + + /// + /// 将object转换为long,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + /// + public static long ParseToLong(this string str, long defaultValue) + { + try + { + return long.Parse(str); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 转换为int + /// + /// 将object转换为int,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static int ParseToInt(this object str) + { + try + { + return Convert.ToInt32(str); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为int,若转换失败,则返回指定值。不抛出异常。 + /// null返回默认值 + /// + /// + /// + /// + public static int ParseToInt(this object str, int defaultValue) + { + if (str == null) + { + return defaultValue; + } + try + { + return Convert.ToInt32(str); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 转换为short + /// + /// 将object转换为short,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static short ParseToShort(this object obj) + { + try + { + return short.Parse(obj.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为short,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static short ParseToShort(this object str, short defaultValue) + { + try + { + return short.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 转换为demical + /// + /// 将object转换为demical,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static decimal ParseToDecimal(this object str, decimal defaultValue) + { + try + { + return decimal.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + + /// + /// 将object转换为demical,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static decimal ParseToDecimal(this object str) + { + try + { + return decimal.Parse(str.ToString()); + } + catch + { + return 0; + } + } + #endregion + + #region 转化为bool + /// + /// 将object转换为bool,若转换失败,则返回false。不抛出异常。 + /// + /// + /// + public static bool ParseToBool(this object str) + { + try + { + return bool.Parse(str.ToString()); + } + catch + { + return false; + } + } + + /// + /// 将object转换为bool,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static bool ParseToBool(this object str, bool result) + { + try + { + return bool.Parse(str.ToString()); + } + catch + { + return result; + } + } + #endregion + + #region 转换为float + /// + /// 将object转换为float,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static float ParseToFloat(this object str) + { + try + { + return float.Parse(str.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为float,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static float ParseToFloat(this object str, float result) + { + try + { + return float.Parse(str.ToString()); + } + catch + { + return result; + } + } + #endregion + + #region 转换为Guid + /// + /// 将string转换为Guid,若转换失败,则返回Guid.Empty。不抛出异常。 + /// + /// + /// + public static Guid ParseToGuid(this string str) + { + try + { + return new Guid(str); + } + catch + { + return Guid.Empty; + } + } + #endregion + + #region 转换为DateTime + /// + /// 将string转换为DateTime,若转换失败,则返回日期最小值。不抛出异常。 + /// + /// + /// + public static DateTime ParseToDateTime(this string str) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return DateTime.MinValue; + } + if (str.Contains("-") || str.Contains("/")) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return DateTime.MinValue; + } + } + + /// + /// 将string转换为DateTime,若转换失败,则返回默认值。 + /// + /// + /// + /// + public static DateTime ParseToDateTime(this string str, DateTime? defaultValue) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return defaultValue.GetValueOrDefault(); + } + if (str.Contains("-") || str.Contains("/")) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return defaultValue.GetValueOrDefault(); + } + } + #endregion + + #region 转换为string + /// + /// 将object转换为string,若转换失败,则返回""。不抛出异常。 + /// + /// + /// + public static string ParseToString(this object obj) + { + try + { + if (obj == null) + { + return string.Empty; + } + else + { + return obj.ToString(); + } + } + catch + { + return string.Empty; + } + } + public static string ParseToStrings(this object obj) + { + try + { + var list = obj as IEnumerable; + if (list != null) + { + return string.Join(",", list); + } + else + { + return obj.ToString(); + } + } + catch + { + return string.Empty; + } + + } + #endregion + + #region 转换为double + /// + /// 将object转换为double,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static double ParseToDouble(this object obj) + { + try + { + return double.Parse(obj.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为double,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + /// + public static double ParseToDouble(this object str, double defaultValue) + { + try + { + return double.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 强制转换类型 + /// + /// 强制转换类型 + /// + /// + /// + /// + public static IEnumerable CastSuper(this IEnumerable source) + { + foreach (object item in source) + { + yield return (TResult)Convert.ChangeType(item, typeof(TResult)); + } + } + #endregion + } +} diff --git a/QYZH.InteractiveMagazine.Common/Extensions/Extension.Validate.cs b/QYZH.InteractiveMagazine.Common/Extensions/Extension.Validate.cs new file mode 100644 index 0000000..67a79e6 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Extensions/Extension.Validate.cs @@ -0,0 +1,44 @@ +//using Microsoft.AspNetCore.Http; + +namespace QYZH.InteractiveMagazine.Common.Extensions +{ + public static partial class Extensions + { + public static bool IsEmpty(this object value) + { + if (value != null && !string.IsNullOrEmpty(value.ParseToString())) + { + return false; + } + else + { + return true; + } + } + public static bool IsNotEmpty(this object value) + { + return !IsEmpty(value); + } + public static bool IsNullOrZero(this object value) + { + if (value == null || value.ParseToString().Trim() == "0") + { + return true; + } + else + { + return false; + } + } + + //public static bool IsAjaxRequest(this HttpRequest request) + //{ + // if (request == null) + // throw new ArgumentNullException("request"); + + // if (request.Headers != null) + // return request.Headers["X-Requested-With"] == "XMLHttpRequest"; + // return false; + //} + } +} diff --git a/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs index 303b882..b576ec8 100644 --- a/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs +++ b/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs @@ -95,5 +95,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions { return s == null || s?.Count() < 1; } + + public static bool IsNull(this object? s) + { + return s == null; + } } } diff --git a/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs index 985dbc0..0fd9aa6 100644 --- a/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs +++ b/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs @@ -163,5 +163,55 @@ namespace QYZH.InteractiveMagazine.Common.Extensions // 匹配域名后的路径部分(包括第一个/) return Regex.Replace(str, @"https?://[^/]+/([^""\s<>]*)", "$1"); } + + public static string AddDomain(this string str, string domainUrl) + { + if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(domainUrl)) return str; + + string pattern = @"src=""([^""]+)"""; + return Regex.Replace(str, pattern, match => + { + string originalUrl = match.Groups[1].Value; + // 忽略base64图片 + if (originalUrl.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return match.Value; // 返回原样 + // 如果已经是完整URL则不做处理 + if (Uri.IsWellFormedUriString(originalUrl, UriKind.Absolute)) return match.Value; // 返回原样 + + + // 处理URL开头可能存在的斜杠 + var trimmedUrl = originalUrl.StartsWith("/") ? originalUrl.Substring(1) : originalUrl; + var trimmedDomain = domainUrl.EndsWith("/") ? domainUrl : domainUrl + "/"; + + string newUrl = $"{trimmedDomain}{trimmedUrl}"; + return $"src=\"{newUrl}\""; // 返回替换后的属性 + }); + } + + public static string ToExtension(this string str) + { + return Regex.Match(str, @"(?<=\.)[a-zA-Z0-9]+$").Value; + } + + public static bool NotNull(this string s) + { + return !string.IsNullOrWhiteSpace(s); + } + + public static List AllUrl(this string str, string url = null) + { + if (string.IsNullOrEmpty(str)) return new List(); + + string pattern = @"src=""([^""]+)"""; + + var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase); + + return matches + .Cast() + .Select(m => m.Groups[1].Value) + .Where(u => !string.IsNullOrEmpty(u) && !u.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) // 排除base64 + .Where(url => !string.IsNullOrEmpty(url) && url.StartsWith(url, StringComparison.OrdinalIgnoreCase)) + //.Where(url => url.StartsWith("https://example.com")) // 二次验证 + .ToList(); + } } } diff --git a/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj b/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj index b44c444..46a33fc 100644 --- a/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj +++ b/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj @@ -1,9 +1,5 @@  - - - - diff --git a/QYZH.InteractiveMagazine.IService/IJournalCatalogService.cs b/QYZH.InteractiveMagazine.IService/IJournalCatalogService.cs new file mode 100644 index 0000000..092453f --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IJournalCatalogService.cs @@ -0,0 +1,45 @@ +using QYZH.InteractiveMagazine.Models; +using QYZH.InteractiveMagazine.Models.Base; +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +public interface IJournalCatalogService : IBaseService +{ + Task> DetailAsync(long JournalId); + + Task InsertAsync(JournalCatalogInput input); + + Task ImportAsync(JournalImportDto input); + + Task UpdateAsync(JournalCatalogUpdateInput input); + + Task DeleteAsync(long id); + + Task MoveAsync(MoveInput input); + + //Task CopyAsync(CopyInput input); + + /// + /// 获取书分类及页码 + /// + /// + /// + Task> GetJournalCatalogListAsync(long JournalId); + + /// + /// 获取书分类及页码 + /// + /// + /// + Task> GetJournalCataloTreeAsync(long JournalId); + + /// + /// 导入书籍目录 + /// + /// + /// + /// + Task ImportCatalogAsync(long JournalId, List dtos); +} diff --git a/QYZH.InteractiveMagazine.IService/IJournalPageService.cs b/QYZH.InteractiveMagazine.IService/IJournalPageService.cs new file mode 100644 index 0000000..7b1e1bc --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IJournalPageService.cs @@ -0,0 +1,32 @@ + + +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +public interface IJournalPageService: IBaseService +{ + Task InsertAsync(JournalAddV2Input input); + + Task UpdateAsync(PageLayoutInput input); + + /// + /// 修改书页的点阵码 + /// + /// + /// + Task UpdatePageNoAsync(long JournalId); + + /// + /// 打印书页(全部) + /// + /// + /// + Task PrintJournalPageAsync(long JournalId); + + Task DetailAsync(long id); + Task DeleteAsync(long id); + + //Task> PageNoArticleAsync(long id); +} diff --git a/QYZH.InteractiveMagazine.IService/IJournalPageTaskService.cs b/QYZH.InteractiveMagazine.IService/IJournalPageTaskService.cs new file mode 100644 index 0000000..24faa58 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IJournalPageTaskService.cs @@ -0,0 +1,17 @@ + + +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +public interface IJournalPageTaskService : IBaseService +{ + Task InsertAsync(JournalPageTaskAddInput input); + Task UpdateAsync(JournalPageTaskUpdateInput input); + Task KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input); + Task DetailAsync(long id); + + Task DeleteAsync(long id); + Task ComplementAsync(JournalPageTaskComplementInput input); +} diff --git a/QYZH.InteractiveMagazine.IService/IJournalService.cs b/QYZH.InteractiveMagazine.IService/IJournalService.cs new file mode 100644 index 0000000..eed5155 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IJournalService.cs @@ -0,0 +1,48 @@ + +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Dto.DotMatrix; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +public interface IJournalService : IBaseService +{ + /// + /// 查询List + /// + /// + /// + Task> GetListAsync(JournalQueryDto dto); + + /// + /// 分页查询 + /// + /// + /// + Task> GetPageListAsync(PageQueryModel search); + + + /// + /// 编辑 + /// + /// + /// + Task> EditAsync(JournalEditDto input); + + Task> Tasks(long id); + + Task DetailAsync(long id); + + Task DeleteAsync(List id); + + + Task StartPageAsync(long Id, int Index); + + Task StatusAsync(long id, JournalStatusEnum status); + + Task PrintCodeAsync(long id); + + Task ResultReportAsync(DotMatrixNoteJournalReportInput input); +} diff --git a/QYZH.InteractiveMagazine.IService/IPetService.cs b/QYZH.InteractiveMagazine.IService/IPetService.cs index a611d8c..c3a491a 100644 --- a/QYZH.InteractiveMagazine.IService/IPetService.cs +++ b/QYZH.InteractiveMagazine.IService/IPetService.cs @@ -37,6 +37,14 @@ public interface IPetService : IBaseService /// 喂养结果 Task FeedPetAsync(long userId, FeedPetInput input); + /// + /// 喂养宠物(无事务,需在外部事务中调用) + /// + /// 用户Id + /// 喂养输入 + /// 喂养结果 + Task FeedPetInTranAsync(long userId, FeedPetInput input); + /// /// 获取宠物喂养记录列表 /// diff --git a/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs b/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs index 74c17ce..2d7fa51 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs @@ -2,7 +2,6 @@ using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; -using QYZH.InteractiveMagazine.Infrastructure.MessageQueue; using QYZH.InteractiveMagazine.Models.Settings; using RabbitMQ.Client; using StackExchange.Redis; @@ -25,31 +24,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs var source = friendlyName.Split('.'); var assemblyNames = string.Join(".", source.Take(source.Length - 1)); containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames)); - - InitializeRabbitMQ(c.Configuration, containerBuilder); }); } - - - private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder) - { - var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get(); - if (rabbitMQSettings != null && !string.IsNullOrWhiteSpace(rabbitMQSettings.HostName)) - { - var factory = new ConnectionFactory - { - HostName = rabbitMQSettings.HostName, - Port = rabbitMQSettings.Port, - UserName = rabbitMQSettings.UserName ?? string.Empty, - Password = rabbitMQSettings.Password ?? string.Empty, - VirtualHost = rabbitMQSettings.VirtualHost ?? string.Empty - }; - - var connection = factory.CreateConnectionAsync().GetAwaiter().GetResult(); - - containerBuilder.RegisterInstance(connection).As().SingleInstance(); - containerBuilder.RegisterType().InstancePerDependency(); - } - } } } diff --git a/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs b/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs deleted file mode 100644 index 18021c8..0000000 --- a/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.Extensions.Logging; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; -using System.Text; - -namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue; - -/// -/// RabbitMQ消息消费者基类 -/// -public abstract class RabbitMQConsumer : IDisposable -{ - private readonly IConnection _connection; - private readonly ILogger _logger; - private IChannel? _channel; - private AsyncEventingBasicConsumer? _consumer; - - /// - /// 构造函数 - /// - /// RabbitMQ连接 - /// 日志记录器 - protected RabbitMQConsumer(IConnection connection, ILogger logger) - { - _connection = connection; - _logger = logger; - } - - /// - /// 启动消费 - /// - /// 队列名称 - /// 消息处理委托 - public async Task StartConsume(string queueName, Func handleMessage) - { - _channel = await _connection.CreateChannelAsync(); - - _consumer = new AsyncEventingBasicConsumer(_channel); - _consumer.ReceivedAsync += async (model, ea) => - { - try - { - var body = ea.Body.ToArray(); - var message = Encoding.UTF8.GetString(body); - - await handleMessage(message); - - await _channel.BasicAckAsync(ea.DeliveryTag, false); - - _logger.LogInformation("消息消费成功 | 队列: {QueueName} | 消息: {Message}", queueName, message); - } - catch (Exception ex) - { - _logger.LogError(ex, "消息消费失败 | 队列: {QueueName}", queueName); - await _channel.BasicNackAsync(ea.DeliveryTag, false, true); - } - }; - - await _channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: _consumer); - - _logger.LogInformation("开始消费消息 | 队列: {QueueName}", queueName); - } - - /// - /// 释放资源 - /// - public void Dispose() - { - _channel?.DisposeAsync().GetAwaiter().GetResult(); - GC.SuppressFinalize(this); - } -} diff --git a/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs b/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs deleted file mode 100644 index a84f184..0000000 --- a/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.Extensions.Logging; -using RabbitMQ.Client; -using System.Text; - -namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue; - -/// -/// RabbitMQ消息发布器 -/// -public class RabbitMQPublisher -{ - private readonly IConnection _connection; - private readonly ILogger _logger; - - /// - /// 构造函数 - /// - /// RabbitMQ连接 - /// 日志记录器 - public RabbitMQPublisher(IConnection connection, ILogger logger) - { - _connection = connection; - _logger = logger; - } - - /// - /// 发布消息 - /// - /// 交换机名称 - /// 路由键 - /// 消息内容 - public async Task PublishMessage(string exchange, string routingKey, string message) - { - await using var channel = await _connection.CreateChannelAsync(); - - var body = Encoding.UTF8.GetBytes(message); - - await channel.BasicPublishAsync( - exchange: exchange, - routingKey: routingKey, - body: body - ); - - _logger.LogInformation("消息已发布 | 交换机: {Exchange} | 路由键: {RoutingKey} | 消息: {Message}", - exchange, routingKey, message); - } -} diff --git a/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs index e62f38f..8cdfe25 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs @@ -217,7 +217,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.OSS //var del = _ossResourceRepository.Delete(w => keys.Contains(w.Path)); - //AppException.ThrowIf(!del, "删除资源失败"); + //BusinessException.ThrowIf(!del, "删除资源失败"); return result.Keys.Count() == keys.Count; @@ -286,7 +286,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.OSS // Path = targetObject, //}); - //AppException.ThrowIf(data.IsNull(), "资源添加失败"); + //BusinessException.ThrowIf(data.IsNull(), "资源添加失败"); var req = new CopyObjectRequest(_ossOption.BucketName, sourceObject, _ossOption.BucketName, targetObject) { diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs new file mode 100644 index 0000000..c60c127 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs @@ -0,0 +1,14 @@ +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ +{ + public interface IRabbitMQService + { + Task SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default); + + Task SendBatchAsync(IEnumerable @params, CancellationToken cancellationToken = default); + + Task ReceiveAsync(string queueName, Func callback, CancellationToken cancellationToken = default); + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQConnection.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQConnection.cs new file mode 100644 index 0000000..bd4cc46 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQConnection.cs @@ -0,0 +1,62 @@ +using RabbitMQ.Client; + +namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ +{ + public interface IRabbitMQConnection : IDisposable + { + Task CreateChannel(); + } + + public class RabbitMQConnection : IRabbitMQConnection + { + private readonly ConnectionFactory _factory; + private readonly IConnection _connection; + private bool _isDisposed; + + public RabbitMQConnection(ConnectionFactory factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + _connection = factory.CreateConnectionAsync().Result; + } + + public async Task CreateChannel() + { + EnsureNotDisposed(); + return await _connection.CreateChannelAsync(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + // Free any other managed objects here. + } + + // Free any unmanaged objects here. + _connection.Dispose(); + + _isDisposed = true; + } + + ~RabbitMQConnection() + { + Dispose(false); + } + + private void EnsureNotDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(RabbitMQConnection)); + } + } + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQOptions.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQOptions.cs new file mode 100644 index 0000000..1682f83 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQOptions.cs @@ -0,0 +1,13 @@ +namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ +{ + public class RabbitMQOptions + { + public string UserName { get; set; } + public string Password { get; set; } + public string HostName { get; set; } + public int Port { get; set; } + public string ExchangeName { get; set; } = string.Empty; + + public string VirtualHost { get; set; } = "/"; + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQSendParam.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQSendParam.cs new file mode 100644 index 0000000..68d4711 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQSendParam.cs @@ -0,0 +1,25 @@ +namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ +{ + public class RabbitMQSendParam + { + /// + /// 交换机 默认空 + /// + public string Exchange { get; set; } = ""; + public string Queue { get; set; } + /// + /// 路由键 + /// + public string RoutingKey { get; set; } + + /// + /// 消息数据 + /// + public object Data { get; set; } + + /// + /// 是否清空队列 + /// + public bool Purge { get; set; } = false; + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs new file mode 100644 index 0000000..51023e8 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs @@ -0,0 +1,157 @@ +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ +{ + public class RabbitMQService : IRabbitMQService + { + private readonly IRabbitMQConnection _connection; + private readonly JsonSerializerOptions options = new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + public RabbitMQService(IRabbitMQConnection connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + + public async Task SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default) + { + + try + { + using var channel = await _connection.CreateChannel(); + + // 启用发布者确认 + //var channelOpts = new CreateChannelOptions + //( + // publisherConfirmationsEnabled: true, + // publisherConfirmationTrackingEnabled: true, + // outstandingPublisherConfirmationsRateLimiter: new ThrottlingRateLimiter(MAX_OUTSTANDING_CONFIRMS) + //); + + // 声明队列(持久化) + await channel.QueueDeclareAsync(queue: param.RoutingKey, durable: true, exclusive: false, autoDelete: false, arguments: null); + // 清空队列 + if (param.Purge) await channel.QueuePurgeAsync(param.RoutingKey); + // 消息序列化 + var mesjson = JsonSerializer.Serialize(param.Data, options); + + var body = Encoding.UTF8.GetBytes(mesjson); + var properties = new BasicProperties + { + Persistent = true // 设置消息持久化 + }; + await channel.BasicPublishAsync(param.Exchange, param.RoutingKey, false, properties, body, cancellationToken); + + return true; + + } + catch (OperationCanceledException ex) + { + Console.WriteLine($"Operation was canceled: {ex.Message}"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + } + + + public async Task SendBatchAsync(IEnumerable @params, CancellationToken cancellationToken = default) + { + IChannel channel = null; + try + { + channel = await _connection.CreateChannel(); + + // 开启事务 + await channel.TxSelectAsync(); + + var properties = new BasicProperties + { + Persistent = true // 设置消息持久化 + }; + + // 批量发送消息到不同的 routingKey + var declaredExchanges = new HashSet(); + foreach (var param in @params) + { + // 声明 Exchange(持久化) + if (declaredExchanges.Add(param.Exchange)) + { + await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, autoDelete: false, arguments: null); + } + + // 声明队列(持久化) + await channel.QueueDeclareAsync(queue: param.Queue, durable: true, exclusive: false, autoDelete: false, arguments: null); + + // 绑定队列到 Exchange + await channel.QueueBindAsync(queue: param.Queue, exchange: param.Exchange, routingKey: param.RoutingKey, arguments: null); + + // 清空队列 + if (param.Purge) await channel.QueuePurgeAsync(param.Queue); + + // 消息序列化 + var mesjson = JsonSerializer.Serialize(param.Data, options); + var body = Encoding.UTF8.GetBytes(mesjson); + + // 发布消息 + await channel.BasicPublishAsync(param.Exchange, param.RoutingKey, false, properties, body, cancellationToken); + } + + // 提交事务 - 确保所有消息都发送成功 + await channel.TxCommitAsync(); + + return true; + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + // 回滚事务 + try { await channel?.TxRollbackAsync(); } catch { /* 忽略回滚异常 */ } + return false; + } + finally + { + if (channel != null && !channel.IsClosed) + { + await channel.CloseAsync(); + } + } + } + + + public async Task ReceiveAsync(string queueName, Func callback, CancellationToken cancellationToken = default) + { + var channel = await _connection.CreateChannel(); + //await channel.BasicQosAsync(0, 10, false); // 一次最多接收10条未确认的消息 + await channel.QueueDeclareAsync(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null); + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += async (model, ea) => + { + //var body = ea.Body.ToArray(); + try + { + // 直接传递 model 和 body 给 callback,不需要转换 + await callback(channel, ea); + } + + finally + { + //await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + }; + await channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); + // Prevent the method from returning immediately + await Task.Delay(-1, cancellationToken); + } + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbiteMQExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbiteMQExtensions.cs new file mode 100644 index 0000000..35e6bda --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbiteMQExtensions.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; + +namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ +{ + public static class RabbiteMQExtensions + { + /// + /// 初始化消息队列,并添加Publisher到IoC容器 + /// + /// 从Configuration读取"RabbbitMQOptions配置项" + public static IServiceCollection AddRabbitMQ(this IServiceCollection services, IConfiguration configuration) + { + var rabbitMqSection = configuration.GetSection("RabbitMq"); + + if (rabbitMqSection.Exists()) + { + // 绑定RabbitMQ配置 + services.Configure(rabbitMqSection); + // 注册RabbitMQ连接工厂 + services.AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + var factory = new ConnectionFactory() + { + HostName = options.HostName, + Port = options.Port, + UserName = options.UserName, + Password = options.Password, + VirtualHost = options.VirtualHost, + + // 自动恢复配置 + AutomaticRecoveryEnabled = true, // 启用自动恢复 + NetworkRecoveryInterval = TimeSpan.FromSeconds(10), // 每10秒尝试重连 + // 心跳检测 + RequestedHeartbeat = TimeSpan.FromSeconds(10), // 60秒心跳 + // 其他重要配置 + TopologyRecoveryEnabled = true, // 恢复交换机、队列等拓扑结构 + RequestedConnectionTimeout = TimeSpan.FromSeconds(30), // 连接超时 + SocketReadTimeout = TimeSpan.FromSeconds(30), // 读取超时 + SocketWriteTimeout = TimeSpan.FromSeconds(30) // 写入超时 + }; + return new RabbitMQConnection(factory); + }); + + // 添加RabbitMQService的服务注册 + services.AddSingleton(); + //services.AddHostedService(); + } + + return services; + } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs b/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs index 39b7b20..93e17e5 100644 --- a/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs +++ b/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs @@ -43,4 +43,23 @@ public class BusinessException : Exception public BusinessException(string message, Exception innerException) : base(message, innerException) { } + + public static void ThrowIf(bool isTrue, string message) + { + if (isTrue) throw new BusinessException(message); + } + + public static void ThrowIf(bool isTrue, Func action) + { + if (isTrue) throw new BusinessException(action.Invoke()); + } + + public static void ThrowIf(bool isTrue, string message, Action action) + { + if (isTrue) + { + action(); + throw new BusinessException(message); + } + } } diff --git a/QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixNotebookReportInput.cs b/QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixNotebookReportInput.cs new file mode 100644 index 0000000..c858c40 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixNotebookReportInput.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.DotMatrix +{ + public class DotMatrixNoteJournalReportInput + { + public long Id { get; set; } + + public bool Success { get; set; } + + public string FileKey { get; set; } + + public string FileUrl { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixOutput.cs new file mode 100644 index 0000000..01679cb --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/DotMatrix/DotMatrixOutput.cs @@ -0,0 +1,73 @@ + +using QYZH.InteractiveMagazine.Common.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.DotMatrix +{ + public class DotMatrixOutput + { + /// + /// 方法 + /// + public string Method { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// id + /// + public long NoteId { get; set; } + + private string _pdfUrl; + /// + /// pdf地址 + /// + public string PdfUrl + { + get => DomainHelper.OssFullUrl(_pdfUrl); + set => _pdfUrl = value; + } + + public List DotMatrixs { get; set; } = new List(); + } + + public class DotMatrixMqOutput + { + public long DotId { get; set; } + + private string _fileAddress; + /// + /// 点阵xml地址 + /// + public string FileAddress + { + get => DomainHelper.OssFullUrl(_fileAddress); + set => _fileAddress = value; + } + + /// + /// 页 + /// + public List Pages { get; set; } = new List(); + } + + public class DotMatrixPageDto + { + /// + /// 页码 + /// + public string PageNo { get; set; } + + /// + /// 连续数量 + /// + public int PageNum { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs new file mode 100644 index 0000000..12ef5ad --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs @@ -0,0 +1,81 @@ +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.Models.Dto; + +/// +/// 绑定期刊输入DTO +/// +public class BindJournalInput +{ + /// + /// 期刊模板Id(扫码解析的期刊定义Id) + /// + public long JournalId { get; set; } + + /// + /// 实例化期刊Id(扫码解析的具体期刊实例Id,可选) + /// + public long Id { get; set; } + + /// + /// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe + /// + public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString(); +} + +/// +/// 绑定期刊输出DTO +/// +public class BindJournalOutput +{ + /// + /// 绑定记录Id + /// + public long Id { get; set; } + + /// + /// 用户Id + /// + public long UserId { get; set; } + + /// + /// 期刊模板Id + /// + public long JournalId { get; set; } + + /// + /// 关联类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 状态 + /// + public string Status { get; set; } = string.Empty; + + /// + /// 绑定时间 + /// + public DateTime CreatedAt { get; set; } +} + +/// +/// 用户期刊关联查询输入DTO +/// +public class UserJournalQueryInput : PageQueryModel +{ + /// + /// 期刊模板Id + /// + public long? JournalId { get; set; } + + /// + /// 实例化期刊Id + /// + public long Id { get; set; } + + /// + /// 关联类型: Read, Favorite, Subscribe + /// + public string? Type { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogAddInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogAddInput.cs new file mode 100644 index 0000000..139e73e --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogAddInput.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Text.Json.Serialization; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + + public class JournalImportDto + { + /// + /// 书Id + /// + public long JournalId { get; set; } + + /// + /// pdf地址 + /// + public string PdfUrl { get; set; } + + /// + /// 从多少页开始 + /// + public int Index { get; set; } + + public List JournalCatalogs { get; set; } + } + + /// + /// 目录导入 + /// + public class JournalCatalogImportDto + { + /// + /// 一级目录 + /// + public string ParentName { get; set; } + + /// + /// 二级目录 + /// + public string Name { get; set; } + + /// + /// 页码 + /// + public int PageNum { get; set; } + + [JsonIgnore] + public long PagePageId { get; set; } + } + + public class JournalCatalogAddInput + { + /// + /// 目录名称 + /// + public string Name { get; set; } + + /// + /// 级别 + /// + public int Level { get; set; } + + /// + /// 类型0-目录 1-页 + /// + public int Type { get; set; } + + /// + /// url + /// + public string Url { get; set; } + } + + public class JournalCatalogInput + { + /// + /// 书Id + /// + public long JournalId { get; set; } + + /// + /// 目录名称 + /// + public string Name { get; set; } + + /// + /// 父Id;无限级别 + /// + public long ParentId { get; set; } + + /// + /// 等级 + /// + public int Level { get; set; } + + /// + /// 指定插入位置的索引(从0开始)不传或传null时默认追加到末尾 + /// + public int? Position { get; set; } + + /// + /// 类型0-目录 1-页 + /// + public int Type { get; set; } + + /// + /// url + /// + public string Url { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogTreeListDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogTreeListDto.cs new file mode 100644 index 0000000..efafae8 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogTreeListDto.cs @@ -0,0 +1,71 @@ +using MiniExcelLibs.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public partial class JournalCatalogTreeListDto + { + /// + /// Desc:主键id + /// + [ExcelIgnore] + public long Id { get; set; } + + /// + /// Desc:书id + /// + [ExcelIgnore] + public long JournalId { get; set; } + + /// + /// Desc:一级目录名称 + /// + [DisplayName("目录名称")] + [Required] + public string? ParentName { get; set; } + + /// + /// Desc:目录名称 + /// + [DisplayName("二级目录")] + [Required] + public string Name { get; set; } + + [ExcelIgnore] + public long PageId { get; set; } + + [DisplayName("页码")] + [Required] + public int PageNum { get; set; } + + [ExcelIgnore] + public long? ParentId { get; set; } + } + + + public partial class IcrJournalCatalogTreeDto + { + /// + /// Desc:主键id + /// + public long Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 层级 + /// + public int Level { get; set; } + + public List Child { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogUpdateInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogUpdateInput.cs new file mode 100644 index 0000000..1d21a2d --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalCatalogUpdateInput.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalCatalogUpdateInput + { + public long Id { get; set; } + + /// + /// 目录名称 + /// + public string Name { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs index 12ef5ad..3fd81b2 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs @@ -1,81 +1,145 @@ +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Common.Helpers; using QYZH.InteractiveMagazine.Models.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -namespace QYZH.InteractiveMagazine.Models.Dto; - -/// -/// 绑定期刊输入DTO -/// -public class BindJournalInput +namespace QYZH.InteractiveMagazine.Models.Dto.Journal { - /// - /// 期刊模板Id(扫码解析的期刊定义Id) - /// - public long JournalId { get; set; } + public class JournalDto + { + /// + /// Desc:主键id + /// + public long Id { get; set; } - /// - /// 实例化期刊Id(扫码解析的具体期刊实例Id,可选) - /// - public long Id { get; set; } + /// + /// Desc:书籍名称 + /// + public string Name { get; set; } - /// - /// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe - /// - public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString(); -} - -/// -/// 绑定期刊输出DTO -/// -public class BindJournalOutput -{ - /// - /// 绑定记录Id - /// - public long Id { get; set; } - - /// - /// 用户Id - /// - public long UserId { get; set; } - - /// - /// 期刊模板Id - /// - public long JournalId { get; set; } - - /// - /// 关联类型 - /// - public string Type { get; set; } = string.Empty; - - /// - /// 状态 - /// - public string Status { get; set; } = string.Empty; - - /// - /// 绑定时间 - /// - public DateTime CreatedAt { get; set; } -} - -/// -/// 用户期刊关联查询输入DTO -/// -public class UserJournalQueryInput : PageQueryModel -{ - /// - /// 期刊模板Id - /// - public long? JournalId { get; set; } - - /// - /// 实例化期刊Id - /// - public long Id { get; set; } - - /// - /// 关联类型: Read, Favorite, Subscribe - /// - public string? Type { get; set; } + /// + /// Desc:总页数 + /// + public int TotalPage { get; set; } + + /// + /// Desc:校验页数 + /// + public int VerifyPage { get; set; } + + + + /// + /// Desc:状态 + /// + public JournalStatusEnum Status { get; set; } + + /// + /// 状态 + /// + public string ShowStatus => Status.GetDescription(); + + private string _pdfUrl; + /// + /// pdf预览地址 + /// + public string PdfUrl + { + get => DomainHelper.OssFullUrl(_pdfUrl); + set => _pdfUrl = value; + } + + /// + /// Desc:宽度 + /// + public float Width { get; set; } + + /// + /// Desc:高度 + /// + public float Height { get; set; } + + /// + /// Desc:乐观锁 + /// + public int Revision { get; set; } + + /// + /// Desc:审查结果(默认0 1通过 -1驳回) + /// + public int Result { get; set; } + + private string _cover; + /// + /// 封面 + /// + public string Cover + { + get => DomainHelper.OssFullUrl(_cover); + set => _cover = value; + } + + private string _backCover; + /// + /// 封底 + /// + public string BackCover + { + get => DomainHelper.OssFullUrl(_backCover); + set => _backCover = value; + } + + + private string _pdfPreviewUrl; + + /// + /// pdf预览地址 + /// + public string PdfPreviewUrl + { + get => DomainHelper.OssFullUrl(_pdfPreviewUrl); + set => _pdfPreviewUrl = value; + } + + /// + /// Desc:结论json + /// + public object? Conclusion { get; set; } + + /// + /// Desc:思路导读 + /// + public string? Guide { get; set; } + + /// + /// Desc:家长指导 + /// + public string? Tutelage { get; set; } + + /// + /// Desc:所属机构列表 + /// + public Dictionary? Organization { get; set; } + + //public List> Organizations => Organization.Select(c => new Dictionary() { { c.Key, c.Value } }).ToList(); + + /// + /// Desc:副标题 + /// + public string? Title { get; set; } + + /// + /// 年级 + /// + public int Grade { get; set; } + + /// + /// Desc:下载书籍页码点阵码PDF文件名称 + /// + public string? DownloadJournalPagePdfName { get; set; } + } } diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalEditDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalEditDto.cs new file mode 100644 index 0000000..3e9fb77 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalEditDto.cs @@ -0,0 +1,116 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public partial class JournalEditDto + { + /// + /// Desc:主键id + /// + public long Id { get; set; } + + /// + /// Desc:书籍名称 + /// + public string Name { get; set; } + + /// + /// Desc:总页数 + /// + public int TotalPage { get; set; } + + /// + /// Desc:校验页数 + /// + public int VerifyPage { get; set; } + + + /// + /// Desc:状态 + /// + public JournalStatusEnum Status { get; set; } = JournalStatusEnum.Created; + + /// + /// Desc:pdf地址 + /// + public string PdfUrl { get; set; } + + /// + /// Desc:宽度 + /// + public float Width { get; set; } + + /// + /// Desc:高度 + /// + public float Height { get; set; } + + /// + /// Desc:乐观锁 + /// + [JsonIgnore] + public int Revision { get; set; } + + /// + /// Desc:审查结果(默认0 1通过 -1驳回) + /// + public int Result { get; set; } + + /// + /// Desc:封面 + /// + public string Cover { get; set; } + + /// + /// Desc:封底 + /// + public string BackCover { get; set; } + + /// + /// Desc:pdf预览地址 + /// + public string PdfPreviewUrl { get; set; } + + /// + /// Desc:结论json + /// + public object Conclusion { get; set; } + + /// + /// Desc:思路导读 + /// + public string Guide { get; set; } + + /// + /// Desc:家长指导 + /// + [JsonIgnore] + public string Tutelage { get; set; } + + /// + /// Desc:所属机构 + /// + public long[] OrganizationIds { get; set; } + + ///// + ///// Desc:所属机构 + ///// + //public string OrganizationName { get; set; } + + /// + /// Desc:副标题 + /// + public string Title { get; set; } + + /// + /// 年级 + /// + public int Grade { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageAddV2Input.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageAddV2Input.cs new file mode 100644 index 0000000..7a3d8da --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageAddV2Input.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalAddV2Input + { + /// + /// 书Id + /// + public long JournalId { get; set; } + + /// + /// 书目录id + /// + public long JournalCatalogId { get; set; } + + /// + /// 页码 + /// + public int PageNum { get; set; } + + /// + /// 页图片 + /// + public string Url { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs new file mode 100644 index 0000000..269f64b --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs @@ -0,0 +1,62 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalPageTaskAddInput + { + /// + /// 书Id + /// + public long JournalId { get; set; } + + /// + /// 书页id + /// + public long JournalPageId { get; set; } + + /// + /// 题号 + /// + public string No { get; set; } + + /// + /// 题型 + /// + public TaskBankTypeEnum Type { get; set; } + + /// + /// 学科Id + /// + public long SubjectId { get; set; } + + /// + /// 分数 + /// + public float Score { get; set; } + + /// + /// 回答时间(秒) + /// + public int AnswerTime { get; set; } + + /// + /// 关联的知识点ids + /// + public string KnowledgePointIds { get; set; } + + /// + /// 关键解析 + /// + public string KeywordAnalysis { get; set; } + + /// + /// 问题内容 + /// + public string Task { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskKeywordAnalysis.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskKeywordAnalysis.cs new file mode 100644 index 0000000..704a327 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskKeywordAnalysis.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalPageTaskKeywordAnalysis + { + /// + /// 问题Id + /// + public long Id { get; set; } + + /// + /// 单项解析 + /// + public string KeywordAnalysis { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs new file mode 100644 index 0000000..bff386a --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs @@ -0,0 +1,158 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Common.Helpers; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalPageTaskOutput + { + public long Id { get; set; } + + /// + /// 书Id + /// + public long JournalId { get; set; } + + /// + /// 书页Id + /// + public long JournalPageId { get; set; } + + /// + /// 学科Id + /// + public long SubjectId { get; set; } + + /// + /// 题号 + /// + public string No { get; set; } + + /// + /// 题型 + /// + public TaskBankTypeEnum Type { get; set; } + + public string ShowType => Type.GetDescription(); + + private string _task; + /// + /// 问题 + /// + public string Task + { + get { return _task.AddDomain(DomainHelper.OssDomain); } + set { _task = value; } + } + + /// + /// 选项 + /// + public string Options { get; set; } + + private string _answer; + /// + /// 答案 + /// + public string Answer + { + get { return _answer.AddDomain(DomainHelper.OssDomain); } + set { _answer = value; } + } + + private string _analysis; + /// + /// 解析 + /// + public string Analysis + { + get { return _analysis.AddDomain(DomainHelper.OssDomain); } + set { _analysis = value; } + } + + public string AnalysisUrl { get; set; } + + /// + /// 问题分数 + /// + public float TaskScore { get; set; } + + private string _audioUrl; + /// + /// 音频地址 + /// + public string AudioUrl + { + get { return DomainHelper.OssFullUrl(_audioUrl); } + set { _audioUrl = value; } + } + + private string _videoUrl; + /// + /// 视频地址 + /// + public string VideoUrl + { + get { return DomainHelper.OssFullUrl(_videoUrl); } + set { _videoUrl = value; } + } + + private string _pointsUrl; + /// + /// 点位数据地址 + /// + public string PointsUrl + { + get { return DomainHelper.OssFullUrl(_pointsUrl); } + set { _pointsUrl = value; } + } + + private string _imageUrl; + /// + /// 图片地址 + /// + public string ImageUrl + { + get { return DomainHelper.OssFullUrl(_imageUrl); } + set { _imageUrl = value; } + } + + public List ImageUrls { get; set; } + + /// + /// 问题分数 + /// + public float Score { get; set; } + + /// + /// 回答时间(秒) + /// + public int AnswerTime { get; set; } + + /// + /// 音频开始时间 + /// + public long AudioStartTime { get; set; } + + /// + /// 音频结束时间 + /// + public long AudioEndTime { get; set; } + + /// + /// 知识点 + /// + public string KnowledgePointIds { get; set; } + + /// + /// 回答项解析 + /// + public string KeywordAnalysis { get; set; } + + /// + /// 年级 + /// + public int Grade { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs new file mode 100644 index 0000000..8420e79 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs @@ -0,0 +1,126 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Models.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalPageTaskUpdateInput + { + public long Id { get; set; } + + /// + /// 书Id + /// + public long JournalId { get; set; } + + + /// + /// 书页id + /// + public long JournalPageId { get; set; } + + /// + /// 题号 + /// + public string No { get; set; } + + /// + /// 题型 + /// + public TaskBankTypeEnum Type { get; set; } + + /// + /// 问题 + /// + public string Task { get; set; } + + /// + /// 选项 + /// + public string Options { get; set; } + + /// + /// 答案 + /// + public string Answer { get; set; } + + /// + /// 答案图片 + /// + public string AnswerUrl { get; set; } + + /// + /// 解析 + /// + public string Analysis { get; set; } + + /// + /// 解析图片地址 + /// + public string AnalysisUrl { get; set; } + + /// + /// 问题分数 + /// + public float Score { get; set; } + + /// + /// 回答时间秒 + /// + public int AnswerTime { get; set; } + + /// + /// 图片 + /// + public string ImageUrl { get; set; } + + /// + /// 视频 + /// + public string VideoUrl { get; set; } + + /// + /// 学科Id + /// + public long SubjectId { get; set; } + + /// + /// 关联的知识点ids + /// + public string KnowledgepointIds { get; set; } + } + + public class JournalPageTaskComplementInput + { + public long Id { get; set; } + + /// + /// 任务id + /// + public long AssignTaskId { get; set; } + + /// + /// 音频地址 + /// + public string AudioUrl { get; set; } + + /// + /// 音频开始时间 + /// + public long AudioStartTime { get; set; } + + /// + /// 音频结束时间 + /// + public long AudioEndTime { get; set; } + + /// + /// 点位数据地址 + /// + public string PointsUrl { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageV2Output.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageV2Output.cs new file mode 100644 index 0000000..06de1c9 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageV2Output.cs @@ -0,0 +1,100 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Common.Helpers; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalPageV2Output + { + + + public long JournalId { get; set; } + + public long JournalCatalogId { get; set; } + + public long JournalPageId { get; set; } + + public string Layout { get; set; } + + /// + /// 页码 + /// + public int PageNum { get; set; } + + private string _url; + /// + /// 图片 + /// + public string Url + { + get { return DomainHelper.OssFullUrl(_url); } + set { _url = value; } + } + + /// + /// 问题 + /// + public List? Tasks { get; set; } + + public List? Areas { get; set; } + } + + public class JournalPageTaskV2Output + { + public long Id { get; set; } + public long GroupId { get; set; } + + /// + /// 题号 + /// + public string No { get; set; } + + + //public long test => long.Parse(No.Replace("-", string.Empty)); + + /// + /// 题型 + /// + public TaskBankTypeEnum Type { get; set; } + + public string ShowType => Type.GetDescription(); + + /// + /// 选项 + /// + public string Options { get; set; } + + /// + /// 答案 + /// + public string Answers { get; set; } + + /// + /// 解析 + /// + public string Analysis { get; set; } + + /// + /// 是否分配 + /// + public bool Assign { get; set; } + } + + public class JournalPageOtherOutput + { + public long Id { get; set; } + + /// + /// 题号 + /// + public string No { get; set; } + + /// + /// 题型 + /// + public TaskBankTypeEnum Type { get; set; } + + public string ShowType => Type.GetDescription(); + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQueryDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQueryDto.cs new file mode 100644 index 0000000..e57fbcb --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQueryDto.cs @@ -0,0 +1,22 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalQueryDto + { + /// + /// 书籍名称 + /// + public string? Name { get; set; } + + /// + /// 状态 + /// + public JournalStatusEnum? Status { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQuestionOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQuestionOutput.cs new file mode 100644 index 0000000..4891702 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalQuestionOutput.cs @@ -0,0 +1,35 @@ +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Models.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class JournalTaskOutput + { + public long TaskId { get; set; } + public string TaskNo { get; set; } + + public TaskTypeEnum TaskType + { + get + { + var type = new TaskBankType(); + if (type.ObjectiveTypes.Contains(TaskSubType)) + return TaskTypeEnum.Objective; + else if (type.SubjectiveTypes.Contains(TaskSubType)) + return TaskTypeEnum.Subjective; + else + throw new InvalidOperationException($"Unknown task sub type: {TaskSubType}"); + } + } + + /// + /// 详细类型 + /// + public TaskBankTypeEnum TaskSubType { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/PageLayoutInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/PageLayoutInput.cs new file mode 100644 index 0000000..2323efe --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/PageLayoutInput.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; + +namespace QYZH.InteractiveMagazine.Models.Dto.Journal +{ + public class PageLayoutInput + { + public long Id { get; set; } + + public string Layout { get; set; } + + public List TasksImages { get; set; } + + } + public class TasksImages + { + public long TaskId { get; set; } + + public string Url { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/MoveInput.cs b/QYZH.InteractiveMagazine.Models/Dto/MoveInput.cs new file mode 100644 index 0000000..cbb1abd --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/MoveInput.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Base +{ + public class MoveInput + { + /// + /// 要移动节点ID (必须) + /// + [Required] + public long SourceId { get; set; } + + /// + /// 目标父节点ID (0表示移动到根节点) + /// + public long TargetParentId { get; set; } + + /// + /// 在目标父节点下的插入位置 (可选,从0开始,null表示追加到末尾) + /// + [Range(0, int.MaxValue)] + public int? Position { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/Journal.cs b/QYZH.InteractiveMagazine.Models/Entity/Journal.cs index cfe6ebd..2b8a678 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/Journal.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/Journal.cs @@ -14,73 +14,107 @@ namespace QYZH.InteractiveMagazine.Models.Entity } /// - /// Desc:期刊号 + /// Desc:书籍名称 + /// Default: + /// Nullable:True + /// + public string Name { get; set; } + + /// + /// Desc:总页数 /// Default: /// Nullable:False /// - public int IssueNumber {get;set;} + public int TotalPage { get; set; } - /// - /// Desc:期刊标题 - /// Default: - /// Nullable:False - /// - public string Title {get;set;} + /// + /// Desc:校验页数 + /// Default: + /// Nullable:False + /// + public int VerifyPage { get; set; } - /// - /// Desc:封面图地址 - /// Default: - /// Nullable:True - /// - public string CoverImageUrl {get;set;} - /// - /// Desc:摘要 - /// Default: - /// Nullable:True - /// - public string Summary {get;set;} + /// + /// Desc:pdf地址 + /// Default: + /// Nullable:True + /// + public string PdfUrl { get; set; } - /// - /// Desc:发布时间 - /// Default: - /// Nullable:True - /// - public DateTime? PublishDate {get;set;} + /// + /// Desc:宽度 + /// Default: + /// Nullable:False + /// + public float Width { get; set; } - /// - /// Desc:是否启用 - /// Default:b'1' - /// Nullable:False - /// - public bool IsActive {get;set;} + /// + /// Desc:高度 + /// Default: + /// Nullable:False + /// + public float Height { get; set; } - /// - /// Desc:排序权重 - /// Default:0 - /// Nullable:False - /// - public int SortOrder {get;set;} + /// + /// Desc:乐观锁 + /// Default: + /// Nullable:False + /// + public int Revision { get; set; } - /// - /// Desc:编者寄语 - /// Default: - /// Nullable:True - /// - public string EditorNote {get;set;} + /// + /// Desc:审查结果(默认0 1通过 -1驳回) + /// Default:0 + /// Nullable:False + /// + public int Result { get; set; } - /// - /// Desc:主题色 - /// Default: - /// Nullable:True - /// - public string ThemeColor {get;set;} + /// + /// Desc:封面 + /// Default: + /// Nullable:True + /// + public string Cover { get; set; } + + /// + /// Desc:封底 + /// Default: + /// Nullable:True + /// + public string BackCover { get; set; } + + /// + /// Desc:pdf预览地址 + /// Default: + /// Nullable:True + /// + public string PdfPreviewUrl { get; set; } + + + public string Title { get; set; } + + /// + /// Desc:下载书籍页码点阵码PDF文件名称 + /// Default: + /// Nullable:True + /// + public string DownloadJournalPagePdfName { get; set; } + + + /// + /// Desc:书籍描述/简介 + /// Default: + /// Nullable:True + /// + public string Description { get; set; } + + /// + /// Desc:期刊类型: Normal, Special + /// Default:Normal + /// Nullable:False + /// + public JournalTypeEnum Type {get;set;} - /// - /// Desc:期刊类型: Normal, Special - /// Default:Normal - /// Nullable:False - /// - public JournalTypeEnum Type {get;set;} } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs index 0457435..2a465f4 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs @@ -43,7 +43,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Default: /// Nullable:False /// - public JournalPageTaskTypeEnum Type {get;set;} + public TaskBankTypeEnum Type {get;set;} /// /// Desc:任务 diff --git a/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs index 0d21078..b03a565 100644 --- a/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs +++ b/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs @@ -26,5 +26,5 @@ public enum JournalPageTaskTypeEnum /// 问答题 /// [Description("问答题")] - QuestionAnswer = 4 + TaskAnswer = 4 } diff --git a/QYZH.InteractiveMagazine.Models/Enum/JournalStatusEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/JournalStatusEnum.cs index ea02ab3..3efc38c 100644 --- a/QYZH.InteractiveMagazine.Models/Enum/JournalStatusEnum.cs +++ b/QYZH.InteractiveMagazine.Models/Enum/JournalStatusEnum.cs @@ -8,20 +8,58 @@ namespace QYZH.InteractiveMagazine.Models.Enum; public enum JournalStatusEnum { /// - /// 草稿 + /// 缺失目录 /// - [Description("草稿")] - Draft = 0, + [Description("缺失目录")] + MissCatalog = -1, + /// + /// 已创建 + /// + [Description("已创建")] + Created = 0, + + [Description("编辑")] + Editor = 1, /// - /// 已发布 + /// 已校验 /// - [Description("已发布")] - Published = 1, + [Description("已校验")] + Verify = 3, + + /// + /// 铺码中 + /// + [Description("铺码中")] + Codeing = 4, + + /// + /// 铺码成功 + /// + [Description("铺码成功")] + CodeSuccess = 5, + + /// + /// 铺码失败 + /// + [Description("铺码失败")] + CodeFail = -5, /// /// 已归档 /// [Description("已归档")] - Archived = 2 + Archive = 9, + + /// + /// 已废弃 + /// + [Description("已废弃")] + Abandoned = -9, + + /// + /// 已发布 + /// + [Description("已发布")] + Published = 999 } diff --git a/QYZH.InteractiveMagazine.Models/Enum/TaskBankTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/TaskBankTypeEnum.cs new file mode 100644 index 0000000..0e62bb4 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/TaskBankTypeEnum.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Enum +{ + public enum TaskBankTypeEnum + { + /// + /// 无效题目 + /// + [Description("无效")] + Default = 0, + /// + /// 单选题 + /// + [Description("单选题")] + Single = 1, + + /// + /// 多选题 + /// + [Description("多选题")] + Multiple = 2, + + /// + /// 判断题 + /// + [Description("判断题")] + Whether = 3, + + /// + /// 填空题 + /// + [Description("填空题")] + FillIn = 4, + + /// + /// 应用题 + /// + [Description("应用题")] + Problem = 5, + + /// + /// 简答题 + /// + [Description("简答题")] + ShortAnswer = 6, + + } + + public class TaskBankType + { + /// + /// 客观题 + /// + public TaskBankTypeEnum[] ObjectiveTypes => new[] { TaskBankTypeEnum.Single, TaskBankTypeEnum.Multiple, TaskBankTypeEnum.Whether }; + + /// + /// 获取客观题类型的整数值 + /// + public List ObjectiveTypeValues => ObjectiveTypes.Select(t => (int)t).ToList(); + + /// + /// 主观题 + /// + public TaskBankTypeEnum[] SubjectiveTypes => new[] { TaskBankTypeEnum.FillIn, TaskBankTypeEnum.Problem, TaskBankTypeEnum.ShortAnswer }; + + /// + /// 获取主观题类型的整数值 + /// + public List SubjectiveTypeValues => SubjectiveTypes.Select(t => (int)t).ToList(); + } +} diff --git a/QYZH.InteractiveMagazine.Models/Enum/TaskTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/TaskTypeEnum.cs new file mode 100644 index 0000000..ea32178 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/TaskTypeEnum.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYZH.InteractiveMagazine.Models.Enum +{ + public enum TaskTypeEnum + { + /// + /// 客观题 + /// + [Description("客观题")] + Objective = 1, + + /// + /// 主观题 + /// + [Description("主观题")] + Subjective = 2 + } +} diff --git a/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj b/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj index e339258..8df9756 100644 --- a/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj +++ b/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj @@ -9,9 +9,14 @@ + + + + + diff --git a/QYZH.InteractiveMagazine.Service/CheckInService.cs b/QYZH.InteractiveMagazine.Service/CheckInService.cs index daee141..ffdde75 100644 --- a/QYZH.InteractiveMagazine.Service/CheckInService.cs +++ b/QYZH.InteractiveMagazine.Service/CheckInService.cs @@ -68,7 +68,7 @@ public class CheckInService( .Where(p => p.UserId == userId && !p.IsDeleted) .FirstAsync(); - // 6. 事务执行签到相关写操作 + // 6. 事务执行签到相关写操作(含宠物喂养,统一事务) var result = new CheckInOutput(); await checkInRecordRepository.UseTranAsync(async () => @@ -107,6 +107,17 @@ public class CheckInService( Description = $"签到奖励(连续{consecutiveDays}天)" }); + // 6d. 如果有活跃宠物,喂养宠物(同一事务内) + FeedPetOutput? feedResult = null; + if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0) + { + feedResult = await petService.FeedPetInTranAsync(userId, new FeedPetInput + { + PetId = pet.Id, + GrowthPoints = growthReward + }); + } + // 构建返回结果 result.RecordId = (long)recordId; result.CheckInDate = today; @@ -116,41 +127,17 @@ public class CheckInService( result.PointsBalance = pointsResult.NewBalance; result.GrowthPointsBalance = newGrowthBalance; result.HasPet = pet != null; - }); - - // 7. 如果用户有活跃宠物,调用 PetService 喂养(含进化检查),独立事务 - if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0) - { - try + if (feedResult != null) { - var feedResult = await petService.FeedPetAsync(userId, new FeedPetInput - { - PetId = pet.Id, - GrowthPoints = growthReward - }); - result.HasEvolved = feedResult.HasEvolved; result.EvolvedStageName = feedResult.EvolvedStageName; - - logger.LogInformation("签到成长值已喂养宠物,PetId: {PetId}, 进化: {HasEvolved}", - pet.Id, feedResult.HasEvolved); } - catch (Exception ex) - { - logger.LogWarning(ex, "签到后喂养宠物失败,PetId: {PetId},将创建补偿任务", pet.Id); + }); - await compensationTaskService.CreateTaskAsync(new CreateCompensationTaskInput - { - TaskType = CompensationTaskTypeEnum.PetFeeding, - BusinessSource = "CheckIn", - BusinessId = result.RecordId.ToString(), - UserId = userId, - Payload = new { PetId = pet.Id, GrowthPoints = growthReward }, - ErrorMessage = ex.Message, - ErrorSource = "CheckInService.CheckInAsync → PetService.FeedPetAsync", - MaxRetries = 3 - }); - } + if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0) + { + logger.LogInformation("签到成长值已喂养宠物,PetId: {PetId}, 进化: {HasEvolved}", + pet.Id, result.HasEvolved); } logger.LogInformation("用户签到成功,UserId: {UserId}, 连续{Days}天, 积分+{Points}, 成长值+{Growth}", @@ -339,6 +326,17 @@ public class CheckInService( .ExecuteCommandAsync(); } + // 如果有活跃宠物,喂养宠物(同一事务内) + FeedPetOutput? feedResult = null; + if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0) + { + feedResult = await petService.FeedPetInTranAsync(userId, new Models.Dto.Pet.FeedPetInput + { + PetId = pet.Id, + GrowthPoints = growthReward + }); + } + result.RecordId = (long)recordId; result.CheckInDate = targetDate; result.ConsecutiveDays = 0; @@ -347,27 +345,17 @@ public class CheckInService( result.PointsBalance = pointsResult.NewBalance; result.GrowthPointsBalance = newGrowthBalance; result.HasPet = pet != null; - }); - - // 如果有活跃宠物,喂养成长值 - if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0) - { - try + if (feedResult != null) { - var feedResult = await petService.FeedPetAsync(userId, new Models.Dto.Pet.FeedPetInput - { - PetId = pet.Id, - GrowthPoints = growthReward - }); - result.HasEvolved = feedResult.HasEvolved; result.EvolvedStageName = feedResult.EvolvedStageName; } - catch (Exception ex) - { - logger.LogWarning(ex, "补签后喂养宠物失败,PetId: {PetId}", pet.Id); - // 补偿机制:如需可在此创建补偿任务 - } + }); + + if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0) + { + logger.LogInformation("补签成长值已喂养宠物,PetId: {PetId}, 进化: {HasEvolved}", + pet.Id, result.HasEvolved); } logger.LogInformation("补签成功,UserId: {UserId}, Date: {Date}, 积分+{Points}, 成长值+{Growth}", diff --git a/QYZH.InteractiveMagazine.Service/JournalCatalogService.cs b/QYZH.InteractiveMagazine.Service/JournalCatalogService.cs new file mode 100644 index 0000000..6faf02e --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/JournalCatalogService.cs @@ -0,0 +1,526 @@ + +using Mapster; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Infrastructure.OSS; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Base; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using SqlSugar; +using Yitter.IdGenerator; + +namespace QYZH.InteractiveMagazine.Service; + +public class JournalCatalogService : BaseRepository, IJournalCatalogService +{ + private readonly BaseRepository _JournalPageRepository; + private readonly BaseRepository _JournalPageTaskRepository; + private readonly OssService _ossService; + + public JournalCatalogService(BaseRepository JournalPageRepository, BaseRepository JournalPageTaskRepository, OssService ossService) + { + //_JournalRepository = JournalRepository; + _JournalPageRepository = JournalPageRepository; + _ossService = ossService; + } + + public async Task> DetailAsync(long JournalId) + { + return await base.Queryable().Where(w => w.ParentId == 0 && w.JournalId == JournalId).OrderBy(o => o.Sort).ToChildListAsync(it => it.ParentId, 0) ?? []; + } + + public async Task ImportAsync(JournalImportDto input) + { + var Journals = await Queryable().Where(w => w.Id == input.JournalId).FirstAsync(); + BusinessException.ThrowIf(Journals.IsNull(), "未找到书本"); + + var pageNum = await _JournalPageRepository.Queryable().Where(w => w.JournalId == input.JournalId).MaxAsync(m => m.PageNum); + BusinessException.ThrowIf(pageNum != 0, "已经存在书页"); + + + var JournalCatalog = new JournalCatalog() + { + Id = YitIdHelper.NextId(), + JournalId = input.JournalId, + Name = "目录", + ParentId = 0, + Level = 0, + Sort = 1, + Type = JournalCatalogTypeEnum.Page, + }; + + int sort = 1; + var result = input.JournalCatalogs.Where(w => w.Type == 1).Select(s => + { + var JournalPage = new JournalPage + { + Id = YitIdHelper.NextId(), + JournalId = input.JournalId, + JournalCatalogId = JournalCatalog.Id, + Sort = sort, + PageNum = sort < input.Index ? 0 : sort - (input.Index - 1) + }; + var key = $"Journal/{input.JournalId}/{JournalPage.Id}/page.{s.Url.ToExtension()}"; + _ossService.CopyObject(s.Url.RemoveDomain(), key); + + JournalPage.Url = key; + //dotMatrixPage.PreviewUrl = key; + + sort = sort + 1; + return new + { + JournalPage = JournalPage + }; + }).ToList(); + + await UseTranAsync(async () => + { + + var data = await base.InsertAsync(JournalCatalog); + + var JournalPages = result.Select(x => x.JournalPage).ToList(); + await _JournalPageRepository.InsertRangeAsync(JournalPages); + + var key = $"Journal/{input.JournalId}/Journal.pdf"; + _ossService.CopyObject(input.PdfUrl.RemoveDomain(), key); + + await Updateable().SetColumns(s => s.PdfUrl, key) + .SetColumns(s => s.Status, JournalStatusEnum.Created) + .SetColumns(s => s.TotalPage, JournalPages.Count).Where(w => w.Id == input.JournalId) + .ExecuteCommandAsync(); + }); + + return result.Count; + } + + + + /// + /// 获取书分类及页码 + /// + /// + /// + public async Task> GetJournalCatalogListAsync(long JournalId) + { + var Journal = await Queryable().Where(w => w.Id == JournalId).FirstAsync(); + BusinessException.ThrowIf(Journal.IsNull(), "未找到书本"); + + var pageNumList = await _JournalPageRepository.Queryable() + .InnerJoin((a, b) => a.JournalCatalogId == b.Id) + .Where((a, b) => a.JournalId == JournalId) + .OrderBy((a, b) => a.PageNum) + .Select((a, b) => new JournalCatalogTreeListDto + { + JournalId = a.JournalId, + Id = b.Id, + Name = b.Name, + PageNum = a.PageNum, + ParentName = SqlFunc.Subqueryable().Where(c => c.Id == b.ParentId).Select(c => c.Name), + ParentId = b.ParentId, + PageId = a.Id, + }) + .ToListAsync(); + + return pageNumList; + } + + + /// + /// 获取书分类及页码 + /// + /// + /// + public async Task> GetJournalCataloTreeAsync(long JournalId) + { + var Journal = await Queryable().Where(w => w.Id == JournalId).FirstAsync(); + BusinessException.ThrowIf(Journal.IsNull(), "未找到书本"); + + // 先查询所有目录 + var allCatalogs = await base.Queryable() + .Where(w => w.JournalId == JournalId) + .OrderBy(o => o.Sort) + .ToListAsync(); + + if (!allCatalogs.Any()) + return new List(); + + // 再查询有页面对应的目录及页面信息 + var pageNumList = await _JournalPageRepository.Queryable() + .InnerJoin((a, b) => a.JournalCatalogId == b.Id) + .Where((a, b) => a.JournalId == JournalId) + .OrderBy((a, b) => a.PageNum) + .Select((a, b) => new JournalCatalogTreeListDto + { + JournalId = a.JournalId, + Id = b.Id, + Name = b.Name, + PageNum = a.PageNum, + ParentName = SqlFunc.Subqueryable().Where(c => c.Id == b.ParentId).Select(c => c.Name), + ParentId = b.ParentId, + PageId = a.Id, + }) + .ToListAsync(); + + var tree = BuildCatalogTree(allCatalogs, pageNumList); + + return tree; + } + + /// + /// 构建目录树形结构(支持任意层级) + /// + /// 所有目录 + /// 有页面对应的目录信息 + /// + private List BuildCatalogTree(List allCatalogs, List pageNumList) + { + // 构建有页面对应的目录到页面列表的映射 + var catalogPagesMap = pageNumList + .GroupBy(x => x.Id) + .ToDictionary( + g => g.Key, + g => g.Select(p => new IcrJournalCatalogTreeDto + { + Id = p.PageId, + Name = $"第{p.PageNum}页", + Level = 0, + Child = new List() + }).ToList() + ); + + // 构建所有目录节点字典 + var catalogDict = new Dictionary(); + var rootCatalogIds = new HashSet(); + + foreach (var catalog in allCatalogs) + { + var catalogId = catalog.Id; + var parentId = catalog.ParentId; + + if (catalogDict.ContainsKey(catalogId)) + continue; + + var node = new IcrJournalCatalogTreeDto + { + Id = catalogId, + Name = catalog.Name, + Level = 0, + Child = catalogPagesMap.GetValueOrDefault(catalogId, new List()) + }; + + catalogDict[catalogId] = node; + + if (parentId == 0) + { + rootCatalogIds.Add(catalogId); + } + } + + // 构建树形结构:将子节点挂载到父节点 + foreach (var catalog in allCatalogs) + { + var catalogId = catalog.Id; + var parentId = catalog.ParentId; + + if (parentId > 0 && catalogDict.ContainsKey(parentId) && catalogDict.ContainsKey(catalogId)) + { + var parentNode = catalogDict[parentId]; + var childNode = catalogDict[catalogId]; + + if (!parentNode.Child.Any(c => c.Id == childNode.Id)) + { + parentNode.Child.Add(childNode); + } + } + } + + // 计算每个节点的层级并返回根节点列表 + var result = rootCatalogIds + .Where(id => catalogDict.ContainsKey(id)) + .Select(id => + { + var node = catalogDict[id]; + node.Level = 1; + CalculateChildLevels(node.Child, 2); + return node; + }) + .ToList(); + + return result; + } + + /// + /// 递归计算子节点层级 + /// + private void CalculateChildLevels(List children, int level) + { + foreach (var child in children) + { + child.Level = level; + if (child.Child != null && child.Child.Count > 0) + { + CalculateChildLevels(child.Child, level + 1); + } + } + } + + /// + /// 导入书籍目录 + /// + /// + /// + /// + public async Task ImportCatalogAsync(long JournalId, List dtos) + { + dtos = dtos.Where(c => !string.IsNullOrWhiteSpace(c.Name)).ToList(); + BusinessException.ThrowIf(dtos.Select(c => c.PageNum).Distinct().Count() != dtos.Count(), "存在重复的页码"); + + var Journal = await Queryable().Where(w => w.Id == JournalId).FirstAsync(); + BusinessException.ThrowIf(Journal.IsNull(), "未找到书本"); + var pageNums = dtos.Select(c => c.PageNum).ToList(); + var pageNumNotExists = await _JournalPageRepository.Queryable().AnyAsync(w => w.JournalId == JournalId && !pageNums.Contains(w.PageNum)); + BusinessException.ThrowIf(pageNumNotExists, "不存在的书页"); + + var firstCatelogGroup = dtos.Select(c => c.ParentName).Distinct().Select(c => new JournalCatalog + { + JournalId = JournalId, + Id = YitIdHelper.NextId(), + Level = 1, + Name = c, + }).ToList(); + var subCatelogGroup = dtos.Select(c => new { c.Name, c.ParentName }).Distinct().Select(c => new + { + JournalId = JournalId, + Id = YitIdHelper.NextId(), + Level = 2, + Name = c.Name, + ParentId = firstCatelogGroup.FirstOrDefault(m => m.Name == c.ParentName)?.Id, + ParentName = c.ParentName + }).ToList(); + + var result = await UseTranAsync(async () => + { + await base.DeleteAsync(c => c.JournalId == JournalId); + await base.InsertRangeAsync(firstCatelogGroup); + await base.InsertRangeAsync(subCatelogGroup.Adapt>()); + var JournalPages = await Queryable().Where(c => c.JournalId == JournalId).ToListAsync(); + JournalPages.ForEach(c => + { + var cate = dtos.FirstOrDefault(m => m.PageNum == c.PageNum); + c.JournalCatalogId = subCatelogGroup?.FirstOrDefault(m => m.Name == cate?.Name && m.ParentName == cate?.ParentName)?.Id ?? 0; + }); + await base.Context.Updateable(JournalPages).UpdateColumns(c => c.JournalCatalogId).ExecuteCommandAsync(); + return true; + }); + + return result; + } + + public async Task InsertAsync(JournalCatalogInput input) + { + var map = input.Adapt(); + map.Id = YitIdHelper.NextId(); + await UseTranAsync(async () => + { + // 获取同级所有节点 + var siblings = await base.Queryable().Where(w => w.JournalId == input.JournalId).Where(w => w.ParentId == input.ParentId).OrderBy(o => o.Sort).ToListAsync(); + + if (input.Position.HasValue && input.Position > 0 && input.Position <= siblings.Count) + { + // 调换位置,前面不变,后续加1即可 + int validPosition = Math.Min(input.Position.Value, siblings.Count); + map.Sort = validPosition; + + // 调整后续节点的SortOrder (+1) + await base.Updateable() + .SetColumns(x => x.Sort == x.Sort + 1) + .Where(x => x.ParentId == input.ParentId && x.Sort >= validPosition) + .ExecuteCommandAsync(); + } + else + { + // 默认追加到末尾 + map.Sort = siblings.Count > 0 ? siblings.Max(x => x.Sort) + 1 : 0; + } + + + var data = await base.InsertAsync(map); + + if (input.Type == 1) + { + var Journal = await Queryable().Where(w => w.Id == input.JournalId).FirstAsync(); + BusinessException.ThrowIf(Journal.IsNull(), "未找到书本"); + + //var dotMatrixPage = new DotMatrixPage() + //{ + // Id = YitIdHelper.NextId(), + // DotMatrixNoteJournalId = input.JournalId, + // WidthMilliMeter = Journal.Width, + // HightMilliMeter = Journal.Height, + //}; + var JournalPage = new JournalPage + { + JournalId = input.JournalId, + JournalCatalogId = map.Id, + //DotMatrixPageId = dotMatrixPage.Id, + PageNum = map.Sort, + }; + } + }); + return map.Id; + + } + + public async Task UpdateAsync(JournalCatalogUpdateInput input) + { + var result = await base.Updateable(input.Adapt()).Where(w => w.Id == input.Id).ExecuteCommandAsync() > 0; + + return result; + } + + public async Task DeleteAsync(long id) + { + var catas = await base.Queryable().ToChildListAsync(it => it.ParentId, id); + var ids = catas.Select(s => s.Id).ToList(); + + await UseTranAsync(async () => + { + // 删除目录 + var cata = await base.Deleteable().Where(d => ids.Contains(d.Id)).ExecuteCommandAsync() > 0; + BusinessException.ThrowIf(!cata, $"删除目录失败"); + // 删除所有页/问题 + var pages = await _JournalPageRepository.DeleteAsync(w => ids.Contains(w.JournalCatalogId)); + BusinessException.ThrowIf(pages.IsNull(), $"删除数据失败"); + + + //var x = await _JournalPageRepository.DeleteAsync(w => ids.Contains(w.JournalCatalogId)); + //var y = await _JournalPageTaskRepository.DeleteAsync(w => ids.Contains(w.JournalCatalogId)); + }); + return true; + } + + public async Task MoveAsync(MoveInput input) + { + // 1. 获取源节点并校验 + var sourceNode = await base.Queryable().Where(x => x.Id == input.SourceId).FirstAsync(); + BusinessException.ThrowIf(sourceNode.IsNull(), $"要移动的节点{input.SourceId}不存在"); + + + // 2. 校验目标父节点(如果指定) + if (input.TargetParentId > 0) + { + var targetParentExists = await base.Queryable().Where(x => x.Id == input.TargetParentId).AnyAsync(); + BusinessException.ThrowIf(!targetParentExists, $"目标父节点{input.TargetParentId}不存在"); + } + + await base.UseTranAsync(async () => + { + //var tree = await base.Queryable().Where(x => x.Id == input.SourceId).AsTreeCte().OrderBy(x => x.Level).ToTreeListAsync(); + + if (input.Position.HasValue) + { + await base.Updateable() + .SetColumns(x => x.Sort == x.Sort + 1) + .Where(x => x.ParentId == input.TargetParentId && x.Sort >= input.Position.Value) + .ExecuteCommandAsync(); + } + + // 3. 更新源节点的父节点 + var updateCount = await base.Updateable() + .SetColumns(x => x.ParentId, input.TargetParentId) + .SetColumns(x => x.Sort, input.Position ?? 0) + .Where(x => x.Id == input.SourceId) + .ExecuteCommandAsync() > 0; + + BusinessException.ThrowIf(!updateCount, $"节点移动失败"); + }); + + return true; + } + + + //public async Task CopyAsync(CopyInput input) + //{ + + // // 1. 获取源节点并校验 + // var sourceNode = await base.Queryable().Where(x => x.Id == input.SourceId).FirstAsync(); + // BusinessException.ThrowIf(sourceNode.IsNull(), $"要移动的节点{input.SourceId}不存在"); + + + // // 2. 校验目标父节点(如果指定) + // if (input.TargetParentId > 0) + // { + // var targetParentExists = await base.Queryable().Where(x => x.Id == input.TargetParentId).AnyAsync(); + // BusinessException.ThrowIf(!targetParentExists, $"目标父节点{input.TargetParentId}不存在"); + // } + + // using var uow = _unitOfWorkManager.Begin(); + // var tree = await base.Queryable().IncludeMany(c => c.Pages, then => then.IncludeMany(x => x.PageTasks)) + // .Where(x => x.Id == input.SourceId) + // .AsTreeCte() + // .OrderBy(x => x.Level) + // .ToTreeListAsync(); + + // if (input.Position.HasValue) + // { + // await base.Updateable() + // .Set(x => x.Sort + 1) + // .Where(x => x.ParentId == input.TargetParentId && x.Sort >= input.Position.Value) + // .ExecuteCommandAsync(); + // } + + // var copy = await DeepCopyAsync(tree.First(), input.TargetParentId); + // BusinessException.ThrowIf(copy.IsNull(), $"复制节点失败"); + + // var data = await base.InsertAsync(copy); + // BusinessException.ThrowIf(data.IsNull(), $"复制节点失败"); + + // var pages = copy.Where(s => s.Pages.NotNull()).Queryable()Many(s => s.Pages); + // var tempPage = await _JournalPageRepository.InsertAsync(pages); + // BusinessException.ThrowIf(tempPage.IsNull(), $"复制页面数据失败"); + + // //var task = pages.Where(w => w.PageTasks.NotNull()).Queryable()Many(s => s.PageTasks); + // //var tempPageTask = await _JournalPageTaskRepository.InsertAsync(task); + // //BusinessException.ThrowIf(tempPageTask.IsNull(), $"复制页面问题数据失败"); + + // uow.Commit(); + // return true; + //} + //private async Task> DeepCopyAsync(IcrJournalCatalog source, long targetParentId, List? list = null) + //{ + // list ??= new List(); + + // var copy = new IcrJournalCatalog + // { + // Id = YitIdHelper.NextId(),// 设置新ID + // Name = !list.Any() ? $"{source.Name} 副本" : source.Name, + // ParentId = targetParentId, + // Level = source.Level, + // Sort = source.Sort, + // }; + // copy.Pages?.ForEach(page => + // { + // page.Id = YitIdHelper.NextId(); // 设置新页面ID + // page.JournalCatalogId = copy.Id; // 设置为新目录的ID + // page.PageTasks?.ForEach(task => + // { + // task.Id = YitIdHelper.NextId(); // 设置新问题ID + // task.JournalPageId = page.Id; // 设置为新页面的ID + // }); + // }); + // list.Add(copy); + + // // 递归复制子节点 + // if (source.Childs != null && source.Childs.Any()) + // { + // foreach (var child in source.Childs) + // { + // await DeepCopyAsync(child, copy.Id, list); + // } + // } + // return list; + //} +} diff --git a/QYZH.InteractiveMagazine.Service/JournalPageService.cs b/QYZH.InteractiveMagazine.Service/JournalPageService.cs new file mode 100644 index 0000000..189cc41 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/JournalPageService.cs @@ -0,0 +1,211 @@ + +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Infrastructure.OSS; +using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using System.Diagnostics; +using Yitter.IdGenerator; + +namespace QYZH.InteractiveMagazine.Service; + +public class JournalPageService(BaseRepository JournalRepository, + OssService ossService, + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + IRabbitMQService rabbitMqService, + BaseRepository JournalPageTaskRepository) : BaseRepository, IJournalPageService +{ + + public async Task InsertAsync(JournalAddV2Input input) + { + + var Journal = await JournalRepository.Queryable().Where(w => w.Id == input.JournalId).FirstAsync(); + BusinessException.ThrowIf(Journal.IsNull(), "未找到书本"); + + BusinessException.ThrowIf(Journal?.Status == (int)JournalStatusEnum.Archive, "书籍已归档不能修改"); + + var pages = new List(); + //var dotMatrixpages = new List(); + + //var dotMatrixpage = new DotMatrixPage() + //{ + // Id = YitIdHelper.NextId(), + // DotMatrixNoteJournalId = input.JournalId, + // WidthMilliMeter = Journal.Width, + // HightMilliMeter = Journal.Height, + //}; + + var pageTemp = new JournalPage() + { + JournalId = input.JournalId, + JournalCatalogId = input.JournalCatalogId, + //DotMatrixPageId = dotMatrixpage.Id, + Url = input.Url, + PageNum = input.PageNum, + }; + + //dotMatrixpages.Add(dotMatrixpage); + pages.Add(pageTemp); + + await UseTranAsync(async () => + { + //using var uow = Context.Ado.BeginTran(); + var pageData = await base.InsertRangeAsync(pages); + BusinessException.ThrowIf(pageData.IsNull(), "创建页失败"); + + //var result = await dotMatrixPageRepository.InsertRangeAsync(dotMatrixpages); + //BusinessException.ThrowIf(result, "创建点阵页失败"); + }); + + return pages.Count; // 返回主目录ID + } + + public async Task UpdateAsync(PageLayoutInput input) + { + var page = await Queryable().Where(w => w.Id == input.Id).FirstAsync(); + BusinessException.ThrowIf(page == null, "不存在此页"); + + var Journal = await JournalRepository.GetByIdAsync(page.JournalId); + BusinessException.ThrowIf(Journal == null, "不存在此书"); + + BusinessException.ThrowIf(Journal?.Status == (int)JournalStatusEnum.Archive, "书籍已归档不能修改"); + var transResult = await UseTranAsync(async () => + { + //await dotMatrixPageRepository.Updateable().SetColumns(s => s.Area == input.Layout) + // .SetColumns(s => s.AreaPoints == dotMatrixPage.Area) + // .SetColumns(s => s.WidthMilliMeter == dotMatrixPage.WidthMilliMeter) + // .SetColumns(s => s.HightMilliMeter == dotMatrixPage.HightMilliMeter) + // .SetColumns(s => s.WidthDotMatrix == dotMatrixPage.WidthDotMatrix) + // .SetColumns(s => s.HightDotMatrix == dotMatrixPage.HightDotMatrix) + // .Where(w => w.Id == page.DotMatrixPageId).ExecuteCommandAsync(); + + page.Layout = input.Layout; + base.Update(page); + + input.TasksImages?.ForEach(it => + { + var key = $"Journal/{Journal.Id}/{input.Id}/{it.TaskId}/qustion.{it.Url.ToExtension()}"; + ossService.CopyObject(it.Url.RemoveDomain(), key); + JournalPageTaskRepository.Updateable().SetColumns(s => s.TaskUrl, key).Where(w => w.Id == it.TaskId).ExecuteCommand(); + }); + return true; + }); + return transResult; + } + + /// + /// 修改书页的点阵码 + /// + /// + /// + public async Task UpdatePageNoAsync(long JournalId) + { + //这里不能修改 Exchange = "icr.direct" 否则会报错 + var msRes = await rabbitMqService.SendAsync(new RabbitMQSendParam { Exchange = "icr.direct", Queue = "mq.Journal.updatePageNo.queue", RoutingKey = "mq.Journal.updatePageNo", Data = JournalId });//发生消息 + return msRes; + } + + /// + /// 打印书页(全部) + /// + /// + /// + public async Task PrintJournalPageAsync(long JournalId) + { + //这里不能修改 Exchange = "icr.direct" 否则会报错 + var msRes = await rabbitMqService.SendAsync(new RabbitMQSendParam { Exchange = "icr.direct", Queue = "mq.Journal.printJournalPage.queue", RoutingKey = "mq.Journal.printJournalPage", Data = JournalId });//发生消息 + return msRes; + } + + public async Task DetailAsync(long id) + { + var output = await Queryable() + //.LeftJoin((a, b) => a.DotMatrixPageId == b.Id) + .Where(a => a.Id == id) + .Select(a => new JournalPageV2Output + { + JournalId = a.JournalId, + JournalCatalogId = a.JournalCatalogId, + JournalPageId = a.Id, + Url = a.Url, + Layout = a.Layout, + PageNum = a.PageNum, + }) + .FirstAsync(); + + if (output != null) + { + output.Tasks = await JournalPageTaskRepository.Queryable()//.Select() + .Where(a => a.JournalPageId == output.JournalPageId) + .Select(a => new JournalPageTaskV2Output + { + Id = a.Id, + No = a.No, + Type = (TaskBankTypeEnum)a.Type, + Options = a.Options, + Answers = a.Answer, + Analysis = a.Analysis, + //Assign = SqlFunc.Subqueryable().Where(m => a.Id == m.JournalPageTaskId).Any() + }) + .ToListAsync(); + } + //if (output?.Areas != null) + // output.Areas = await _JournalPageOtherRepository.Queryable().Where(w => w.JournalPageId == output.JournalPageId).Select().ToListAsync(); + + return output; + } + + public async Task DeleteAsync(long id) + { + return await Deleteable().Where(d => d.Id == id).ExecuteCommandAsync() > 0; + } + //public async Task> PageNoArticleAsync(long JournalId) + //{ + // return await Queryable().Where(d => d.JournalId == JournalId) + // .Where(w => w.JournalArticleId == null) + // .ToListAsync(x => new JournalPageNoArticleOutput() + // { + // Id = x.Id, + // PageUrl = x.Url, + // PageNum = x.PageNum + // }); + //} + + private int MillimeterToDotMatrixUnit(float millimeterValue) + { + return (int)(millimeterValue * 8 / 0.3); + } +} + +public class JournalDotPage_Task +{ + public int X { get; set; } + public int Y { get; set; } + public int W { get; set; } + public int H { get; set; } +} + +public class JournalDotPage_Answer +{ + public int X { get; set; } + public int Y { get; set; } + public int W { get; set; } + public int H { get; set; } +} + +public class JournalDotPage_Area +{ + public JournalDotPage_Task Task { get; set; } + + public List AnswerList { get; set; } + + public long TaskId { get; set; } + +} diff --git a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs new file mode 100644 index 0000000..c6532ac --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs @@ -0,0 +1,176 @@ +using Mapster; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Infrastructure.OSS; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using SqlSugar; +using Yitter.IdGenerator; + + +namespace QYZH.InteractiveMagazine.Service; + +public class JournalPageTaskService(BaseRepository journalRepository, OssService ossService) : BaseRepository, IJournalPageTaskService +{ + public async Task InsertAsync(JournalPageTaskAddInput input) + { + var JournalStatus = await journalRepository.Queryable().Where(w => w.Id == input.JournalId).Select(s => s.Status).FirstAsync(); + BusinessException.ThrowIf(JournalStatus == (int)JournalStatusEnum.Archive, "书籍已归档不能添加题目"); + + var map = input.Adapt(); + map.Id = YitIdHelper.NextId(); + map.GroupId = map.Id; + var no = input.No.Split('-').Select(int.Parse).ToArray(); + if (no.Last() >= 2) + { + var startNo = $"{no[0]}-{no[1]}-{no[2]}-1"; + var groupTask = await Queryable().Where(w => w.No == startNo && w.JournalId == input.JournalId).FirstAsync(); + BusinessException.ThrowIf(groupTask.IsNull(), "未找到关联跨页的第一部分"); + map.GroupId = groupTask.GroupId; + map.Score = groupTask.Score; + map.AnswerTime = groupTask.AnswerTime; + map.Type = groupTask.Type; + } + + var data = await base.InsertAsync(map); + return map?.Id; + } + + + public async Task UpdateAsync(JournalPageTaskUpdateInput input) + { + var JournalStatus = await journalRepository.Queryable().Where(w => w.Id == input.JournalId).Select(s => s.Status).FirstAsync(); + BusinessException.ThrowIf(JournalStatus == (int)JournalStatusEnum.Archive, "书籍已归档不能修改题目"); + + var task = await base.Queryable().Where(w => w.Id == input.Id).FirstAsync(); + BusinessException.ThrowIf(task.IsNull(), "未找到关联的题号"); + + var sameTask = await base.Queryable().Where(w => w.No == input.No && w.JournalId == input.JournalId).FirstAsync(); + BusinessException.ThrowIf(sameTask.IsNotEmpty(), "已经存在相同题号题目"); + + + var key = $"Journal/{task.JournalId}/{task.JournalPageId}/{task.GroupId}"; + //if (task.AnswerUrl != input.AnswerUrl && input.AnswerUrl.NotNull()) + //{ + // ossService.CopyObject(input.AnswerUrl.RemoveDomain(), $"{key}/answer.{input.AnswerUrl.ToExtension()}"); + // input.AnswerUrl = $"{key}/answer.{input.AnswerUrl.ToExtension()}"; + //} + //if (task.AnalysisUrl != input.AnalysisUrl && input.AnalysisUrl.NotNull()) + //{ + // ossService.CopyObject(input.AnalysisUrl.RemoveDomain(), $"{key}/analysis.{input.AnalysisUrl.ToExtension()}"); + // input.AnalysisUrl = $"{key}/analysis.{input.AnalysisUrl.ToExtension()}"; + //} + //if (task.VideoUrl != input.VideoUrl && input.VideoUrl.NotNull()) + //{ + // ossService.CopyObject(input.VideoUrl.RemoveDomain(), $"{key}/video.{input.VideoUrl.ToExtension()}"); + // input.VideoUrl = $"{key}/video.{input.VideoUrl.ToExtension()}"; + //} + + input.Task = Deal(task.Task, input.Task, key, "task"); + input.Answer = Deal(task.Answer, input.Answer, key, "answer"); + input.Analysis = Deal(task.Analysis, input.Analysis, key, "analysis"); + + task.Type = input.Type; + task.Task = input.Task; + task.Options = input.Options; + task.Answer = input.Answer; + //task.AnswerUrl = input.AnswerUrl; + task.Analysis = input.Analysis; + //task.AnalysisUrl = input.AnalysisUrl; + task.Score = input.Score; + task.AnswerTime = input.AnswerTime; + // task.VideoUrl = input.VideoUrl; + task.No = input.No; + var no = input.No.Split('-').Select(int.Parse).ToArray(); + if (no.Last() >= 2) + { + var startNo = $"{no[0]}-{no[1]}-{no[2]}-1"; + var groupTask = await Queryable().Where(w => w.No == startNo && w.JournalId == input.JournalId).FirstAsync(); + BusinessException.ThrowIf(groupTask.IsNull(), "未找到关联跨页的第一部分"); + task.GroupId = groupTask.GroupId; + task.Score = groupTask.Score; + task.AnswerTime = groupTask.AnswerTime; + task.Type = groupTask.Type; + } + //task.JournalId = input.JournalId; + //task.JournalPageId = input.JournalPageId; + // 这里不能直接用Set,因为jsonmap无法赋值 不知道为啥 + return await base.Updateable(task).ExecuteCommandAsync() > 0; + } + + public async Task KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input) + { + return await base.Updateable().SetColumns(s => s.KeywordAnalysis, input.KeywordAnalysis).Where(w => w.GroupId == input.Id).ExecuteCommandAsync() > 0; + } + + public async Task DetailAsync(long id) + { + var data = await base.Queryable() + .Where(w => w.Id == id) + .Select(w => new JournalPageTaskOutput() + , true).FirstAsync(); + return data; + + } + + public async Task DeleteAsync(long id) + { + var task = await base.Queryable().Where(w => w.Id == id).FirstAsync(); + BusinessException.ThrowIf(task.IsNull(), "问题不存在"); + + var key = $"Journal/{task.JournalId}/{task.JournalPageId}/{id}"; + + ossService.DeleteObject(key); + + return await base.DeleteAsync(d => d.Id == id); + } + + public async Task ComplementAsync(JournalPageTaskComplementInput input) + { + var task = await base.Queryable().Where(w => w.Id == input.Id).FirstAsync(); + BusinessException.ThrowIf(task.IsNull(), "问题不存在"); + + if (input.AudioUrl.NotNull()) + { + var key = $"Journal/{task.JournalId}/{task.JournalPageId}/{task.Id}/audio.{input.AudioUrl.ToExtension()}"; + ossService.CopyObject(input.AudioUrl, key); + input.AudioUrl = key; + } + if (input.PointsUrl.NotNull()) + { + var key = $"Journal/{task.JournalId}/{task.JournalPageId}/{task.Id}/points.json"; + ossService.CopyObject(input.PointsUrl, key); + input.PointsUrl = key; + } + return await Updateable(input.Adapt()).Where(w => w.Id == input.Id).ExecuteCommandAsync() > 0; + } + + private string Deal(string oldUrls, string newUrls, string key, string type) + { + // 如果 newUrls 为空,则直接返回 + if (newUrls.IsNull()) return null; + + // 处理 oldUrls 为 null 的情况,将其视为空字符串 + var oldTaskUrls = oldUrls.AllUrl().Distinct(); + var taskUrls = newUrls.AllUrl().Distinct().Select(s => s.RemoveDomain()); + + // 删除不存在的(存在于旧集合但不在新集合中) + var removedUrls = oldTaskUrls.Except(taskUrls).ToList(); + ossService.DeleteObjects(removedUrls); + + // 添加新增的(存在于新集合但不在旧集合中) + var addedUrls = taskUrls.Except(oldTaskUrls).ToList(); + foreach (var item in addedUrls) + { + var value = $"{key}/{type}/{YitIdHelper.NextId()}.{item.ToExtension()}"; + ossService.CopyObject(item, value); + + newUrls = newUrls.Replace(item, value); + + } + return newUrls; + } +} diff --git a/QYZH.InteractiveMagazine.Service/JournalService.cs b/QYZH.InteractiveMagazine.Service/JournalService.cs new file mode 100644 index 0000000..594e38b --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/JournalService.cs @@ -0,0 +1,313 @@ +using Mapster; +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Common.Extensions; +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.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using SqlSugar; +using Yitter.IdGenerator; + +namespace QYZH.InteractiveMagazine.Service; + +public class JournalService(BaseRepository JournalPageRepository, + BaseRepository JournalPageTaskRepository, + BaseRepository dotFileRepository, + BaseRepository dotFileDetailRepository, OssService ossService, ILogger logger) : BaseRepository, IJournalService +{ + /// + /// 查询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); + } + + /// + /// 编辑 + /// + /// + /// + public async Task> EditAsync(JournalEditDto input) + { + + var Journal = await base.GetByIdAsync(input.Id); + BusinessException.ThrowIf(Journal.IsNull(), "不存在的Id"); + BusinessException.ThrowIf(Journal.Status == (int)JournalStatusEnum.Archive, "已归档不可编辑"); + 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; + + 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不存在"); + BusinessException.ThrowIf(base.Queryable().Any(w => ids.Contains(w.Id) && w.Status == (int)JournalStatusEnum.Archive), "已归档不可删除"); + var result = await UseTranAsync(async () => + { + await base.DeleteAsync(d => ids.Contains(d.Id)); + await JournalPageRepository.DeleteAsync(d => ids.Contains(d.JournalId)); + 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(), "不存在书"); + BusinessException.ThrowIf(Journal.Status == (int)JournalStatusEnum.Codeing, "正在生成中..."); + + var pages = await JournalPageRepository.Queryable().Where(w => w.JournalId == id).ToListAsync(); + BusinessException.ThrowIf(pages.IsNull(), "不存在页"); + + var output = new DotMatrixOutput() + { + Method = "Journal", + Name = Journal.Name, + NoteId = id, + PdfUrl = Journal.PdfUrl + }; + + + //铺码业务后续调整 + + //var exist = pages.All(c => c.PageNo.IsNull()); + //if (!exist) + //{ + // var dotIds = pages.Select(s => s.DotId).Distinct(); + // var dots = await dotFileRepository.Queryable().Where(w => dotIds.Contains(w.Id)).ToListAsync(); + // output.DotMatrixs = dots.Select(s => new DotMatrixMqOutput() { DotId = s.Id, FileAddress = s.FileAddress }).ToList(); + // foreach (var page in pages) + // { + // var find = output.DotMatrixs.Find(f => f.DotId == page.DotId); + // find!.Pages.Add(new DotMatrixPageDto() + // { + // PageNo = page.PageNo, + // PageNum = 1 + // }); + // } + + // var ok = await producingService.SendAsync(new RabbitMQSendParam() { Exchange = "icr.direct", RoutingKey = "mq.dotmatrix", Data = output }); + // BusinessException.ThrowIf(!ok, "消息发送失败,生成失败"); + + // await base.Updateable().SetColumns(s => s.Status, JournalStatusEnum.Codeing).Where(w => w.Id == id).ExecuteCommandAsync(); + //} + //else + //{ + // //var first = await dotFileDetailRepository.Queryable().Where(w => w.IsUse == false).GroupBy(g => g.DotId).Having(h => h.Count() >= pages.Count).FirstAsync(a => new { DotId = a.Key, Count = a.Count() }); + // var first = await dotFileDetailRepository.Queryable().Where(w => w.IsUse == false).GroupBy(g => g.DotId).Having(h => SqlFunc.AggregateCount(h.Id) >= pages.Count).Select(a => new { DotId = a.DotId, Count = SqlFunc.AggregateCount(a.Id) }).FirstAsync(); + // BusinessException.ThrowIf(first.IsNull(), "点阵页码不足,生成失败"); + // var dotFile = await dotFileRepository.Queryable().Where(w => w.Id == first.DotId).FirstAsync(); + // if (dotFile != null) + // dotFile.Details = dotFileDetailRepository.Queryable().Where(w => !w.IsUse && w.DotId == dotFile.Id).Take(pages.Count()).ToList(); + // var dotMatrix = new DotMatrixMqOutput() + // { + // DotId = dotFile.Id, + // FileAddress = dotFile.FileAddress, + // }; + // output.DotMatrixs.Add(dotMatrix); + + // foreach (var item in dotFile.Details) + // { + // var index = dotFile.Details.IndexOf(item); + // pages[index].PageNo = item.PageName; + // pages[index].DotId = dotFile.Id; + + // dotMatrix.Pages.Add(new DotMatrixPageDto() + // { + // PageNo = item.PageName, + // PageNum = 1 + // }); + // } + // await UseTranAsync(async () => + // { + // // 更新使用数量 + // //await dotFileRepository.Updateable().SetColumns(s => s.TotalUse, dotFile.TotalUse + pages.Count).Where(w => w.Id == dotFile.Id).ExecuteCommandAsync(); + // // 更新使用 + // await dotFileDetailRepository.Updateable().SetColumns(s => s.IsUse, true).Where(w => dotFile.Details.Select(s => s.Id).Contains(w.Id)).ExecuteCommandAsync(); + // // 更新页码 + // await dotMatrixPageRepository.UpdateRangeAsync(pages); + // await Updateable().SetColumns(s => s.Status, JournalStatusEnum.CodeSuccess).Where(w => w.Id == id).ExecuteCommandAsync(); + // //发送消息 + // //var ok = await producingService.SendAsync(new RabbitMQSendParam() { Exchange = "icr.direct", RoutingKey = "mq.dotmatrix", Data = output }); + // //BusinessException.ThrowIf(!ok, "消息发送失败,生成失败"); + // }); + //} + + 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, "未添加任何题目,无法归档"); + 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())}未保存,无法归档"); + var res = await base.Updateable().SetColumns(s => s.Status, status).Where(w => w.Id == id).ExecuteCommandAsync() > 0; + return res; + } +} \ No newline at end of file diff --git a/QYZH.InteractiveMagazine.Service/PetService.cs b/QYZH.InteractiveMagazine.Service/PetService.cs index 94889d1..bc6f01c 100644 --- a/QYZH.InteractiveMagazine.Service/PetService.cs +++ b/QYZH.InteractiveMagazine.Service/PetService.cs @@ -253,79 +253,103 @@ public class PetService( throw new BusinessException("宠物未激活,无法喂养", 400); } + // 事务保证一致性 + await UseTranAsync(async () => + { + result = await FeedPetInTranAsync(userId, input); + }); + + logger.LogInformation("喂养宠物成功,PetId: {PetId}, 成长值: {Before} -> {After}, 进化: {HasEvolved}", + input.PetId, result.GrowthBefore, result.GrowthAfter, result.HasEvolved); + + return result; + } + + /// + /// 喂养宠物(无事务,需在外部事务中调用) + /// + public async Task FeedPetInTranAsync(long userId, FeedPetInput input) + { + // 查询宠物 + var pet = await petRepository.GetByIdAsync(input.PetId); + if (pet == null || pet.IsDeleted) + throw new BusinessException("宠物不存在", 404); + + if (pet.UserId != userId) + throw new BusinessException("无权操作该宠物", 403); + + if (pet.Status != (int)UserPetStatusEnum.Active) + throw new BusinessException("宠物未激活,无法喂养", 400); + var growthBefore = pet.GrowthPoints; var growthAfter = growthBefore + input.GrowthPoints; var hasEvolved = false; string? evolvedStageName = null; - // 事务保证一致性 - await UseTranAsync(async () => + // 累加成长值和喂养次数 + var updateResult = await petRepository.Context.Updateable() + .SetColumns(p => p.GrowthPoints == growthAfter) + .SetColumns(p => p.FeedingCount == p.FeedingCount + 1) + .SetColumns(p => p.UpdatedAt == DateTime.Now) + .SetColumns(p => p.UpdatedBy == userId.ToString()) + .Where(p => p.Id == input.PetId) + .ExecuteCommandAsync(); + + if (updateResult <= 0) { - // 累加成长值和喂养次数 - var updateResult = await petRepository.Context.Updateable() - .SetColumns(p => p.GrowthPoints == growthAfter) - .SetColumns(p => p.FeedingCount == p.FeedingCount + 1) + throw new BusinessException("更新宠物成长值失败", 500); + } + + // 进化检查:查找下一阶段进化形态(PreviousEvolutionId 类型为 long?) + var nextEvolution = await petEvolutionRepository.Queryable() + .Where(e => e.PreviousEvolutionId == pet.CurrentEvolutionId + && e.RequiredGrowth <= growthAfter + && e.Status == (int)DefaultStatusEnum.Active) + .OrderBy(e => e.RequiredGrowth, OrderByType.Desc) + .FirstAsync(); + + if (nextEvolution != null) + { + // 触发进化 + var evolveResult = await petRepository.Context.Updateable() + .SetColumns(p => p.CurrentEvolutionId == nextEvolution.Id) .SetColumns(p => p.UpdatedAt == DateTime.Now) .SetColumns(p => p.UpdatedBy == userId.ToString()) .Where(p => p.Id == input.PetId) .ExecuteCommandAsync(); - if (updateResult <= 0) + if (evolveResult > 0) { - throw new BusinessException("更新宠物成长值失败", 500); + hasEvolved = true; + evolvedStageName = nextEvolution.StageName; + logger.LogInformation("宠物进化成功,PetId: {PetId}, 新形态: {StageName} (Level {StageLevel})", + input.PetId, nextEvolution.StageName, nextEvolution.StageLevel); } + } - // 进化检查:查找下一阶段进化形态(PreviousEvolutionId 类型为 long?) - var nextEvolution = await petEvolutionRepository.Queryable() - .Where(e => e.PreviousEvolutionId == pet.CurrentEvolutionId - && e.RequiredGrowth <= growthAfter - && e.Status == (int)DefaultStatusEnum.Active) - .OrderBy(e => e.RequiredGrowth, OrderByType.Desc) - .FirstAsync(); + // 写入喂养记录 + var record = new PetFeedingRecord + { + PetId = input.PetId, + UserId = userId, + PointsUsed = 0, // 预留:后期可扩展为消耗积分喂养 + GrowthChange = input.GrowthPoints, + GrowthBefore = growthBefore, + GrowthAfter = growthAfter, + Type = PetFeedingRecordTypeEnum.Normal, + Status = (int)PetFeedingRecordStatusEnum.Success, + IsDeleted = false, + CreatedBy = userId.ToString(), + CreatedAt = DateTime.Now, + UpdatedBy = userId.ToString(), + UpdatedAt = DateTime.Now + }; - if (nextEvolution != null) - { - // 触发进化 - var evolveResult = await petRepository.Context.Updateable() - .SetColumns(p => p.CurrentEvolutionId == nextEvolution.Id) - .SetColumns(p => p.UpdatedAt == DateTime.Now) - .SetColumns(p => p.UpdatedBy == userId.ToString()) - .Where(p => p.Id == input.PetId) - .ExecuteCommandAsync(); - - if (evolveResult > 0) - { - hasEvolved = true; - evolvedStageName = nextEvolution.StageName; - logger.LogInformation("宠物进化成功,PetId: {PetId}, 新形态: {StageName} (Level {StageLevel})", - input.PetId, nextEvolution.StageName, nextEvolution.StageLevel); - } - } - - // 写入喂养记录 - var record = new PetFeedingRecord - { - PetId = input.PetId, - UserId = userId, - PointsUsed = 0, // 预留:后期可扩展为消耗积分喂养 - GrowthChange = input.GrowthPoints, - GrowthBefore = growthBefore, - GrowthAfter = growthAfter, - Type = PetFeedingRecordTypeEnum.Normal, - Status = (int)PetFeedingRecordStatusEnum.Success, - IsDeleted = false, - CreatedBy = userId.ToString(), - CreatedAt = DateTime.Now, - UpdatedBy = userId.ToString(), - UpdatedAt = DateTime.Now - }; - - var insertResult = await feedingRecordRepository.InsertAsync(record); - if (!insertResult) - { - throw new BusinessException("写入喂养记录失败", 500); - } - }); + var insertResult = await feedingRecordRepository.InsertAsync(record); + if (!insertResult) + { + throw new BusinessException("写入喂养记录失败", 500); + } logger.LogInformation("喂养宠物成功,PetId: {PetId}, 成长值: {Before} -> {After}, 进化: {HasEvolved}", input.PetId, growthBefore, growthAfter, hasEvolved); diff --git a/QYZH.InteractiveMagazine.Service/UsersService.cs b/QYZH.InteractiveMagazine.Service/UsersService.cs index 445ea48..8059902 100644 --- a/QYZH.InteractiveMagazine.Service/UsersService.cs +++ b/QYZH.InteractiveMagazine.Service/UsersService.cs @@ -158,7 +158,7 @@ public class UsersService( if (journalDict.TryGetValue(item.JournalId, out var journal)) { item.JournalTitle = journal.Title; - item.CoverImageUrl = journal.CoverImageUrl; + item.CoverImageUrl = journal.Cover; } } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs index 734a48c..100eaf0 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs @@ -1,8 +1,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using MiniExcelLibs; +using QYZH.InteractiveMagazine.Infrastructure.Context; +using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Enum; using System.Security.Claims; +using System.Web; namespace QYZH.InteractiveMagazine.WebApi.Controllers; @@ -75,4 +79,57 @@ public abstract class BaseController : ControllerBase { return Ok(BaseResponse.Fail(msg)); } + + /// + /// 导出Excel + /// + /// 完整文件路径 + /// 带扩展文件名 + /// + protected IActionResult ExportExcel(string path, string fileName) + { + //var webHostEnvironment = App.WebHostEnvironment; + if (!Path.Exists(path)) + { + throw new BusinessException(fileName + "文件不存在"); + } + var stream = System.IO.File.OpenRead(path); //创建文件流 + + Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition"); + return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", HttpUtility.UrlEncode(fileName)); + } + + protected (string, string) DownloadImportTemplate(List list, string fileName) + { + IWebHostEnvironment webHostEnvironment = ServiceContext.GetService(); + string sFileName = $"{fileName}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "ImportTemplate", sFileName); + + //不存在模板创建模板 + if (!Directory.Exists(fullPath)) + { + var directoryPath = Path.GetDirectoryName(fullPath); + if (directoryPath != null) + { + Directory.CreateDirectory(directoryPath); + } + } + if (!Path.Exists(fullPath)) + { + MiniExcel.SaveAs(fullPath, list, overwriteFile: true); + } + return (sFileName, fullPath); + } + + protected (string, string) ExportExcelMini(List list, string sheetName, string fileName) + { + IWebHostEnvironment webHostEnvironment = ServiceContext.GetService(); + string sFileName = $"{fileName}_{DateTime.Now:MMdd_HHmmss}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "export", sFileName); + + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + + MiniExcel.SaveAs(fullPath, list, sheetName: sheetName); + return (sFileName, fullPath); + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs new file mode 100644 index 0000000..c37c484 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs @@ -0,0 +1,559 @@ + +using QYZH.InteractiveMagazine.Models.Base; +using QYZH.InteractiveMagazine.Models.Dto.Journal; +using QYZH.InteractiveMagazine.Models.Dto.DotMatrix; +using QYZH.InteractiveMagazine.Models.Enum; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MiniExcelLibs; +using QYZH.InteractiveMagazine.Infrastructure.OSS; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.WebApi.Controllers; +using System.ComponentModel.DataAnnotations; +using System.Web; + + +namespace QYZH.InteractiveMagazine.WebApi.Controllers +{ + /// + /// 期刊管理 + /// + [ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] + [Route("api/icr")] + public class JournalController( + OssService ossService, + IJournalCatalogService JournalCatalogService, + IJournalService JournalService, + IJournalPageService JournalPageService, + IJournalPageTaskService JournalPageTaskService, + IHttpClientFactory httpClientFactory, + Microsoft.Extensions.Configuration.IConfiguration configuration) : BaseController + { + + #region 制书 + + /// + /// 根据ID查询书籍数据 + /// + /// + /// + [HttpGet, Route("Journal/detail/{id:long}")] + public async Task> GetByIdAsync([Required] long id) + { + var result = await JournalService.GetByExpressionAsync(c => c.Id == id); + return BaseResponse.Success(result); + } + + /// + /// 分页查询书籍列表 + /// + /// 查询条件 + /// + [HttpPost, Route("Journal/pagelist")] + public async Task>> GetPageListAsync([FromBody] PageQueryModel search) + { + var result = await JournalService.GetPageListAsync(search); + return BaseResponse>.Success(result); + } + + /// + /// 查询书籍列表 + /// + /// + /// + [HttpPost, Route("Journal/list")] + public async Task>> GetListAsync([FromQuery] JournalQueryDto dto) + { + var result = await JournalService.GetListAsync(dto); + return BaseResponse>.Success(result); + } + + ///// + ///// 创建书籍 + ///// + ///// 书籍对象 + ///// + //[HttpPost, Route("Journal/add")] + //public async Task> AddAsync([FromBody] JournalEditDto dto) + //{ + // var result = await JournalService.AddAsync(dto); + // return result; + //} + + /// + /// 编辑书籍 + /// + /// 书籍对象 + /// + [HttpPost, Route("Journal/update")] + public async Task Edit([FromBody] JournalEditDto dto) + { + var result = await JournalService.EditAsync(dto); + return result; + } + + /// + /// 批量删除 + /// + /// + /// + [HttpPost, Route("Journal/delete")] + public async Task> DeleteAsync([Required][FromBody] List ids) + { + var result = await JournalService.DeleteAsync(ids); + return BaseResponse.Success(result); + } + + /// + /// 书籍起始页 + /// + /// + [HttpPost, Route("Journal/startpage")] + public async Task> StartPageAsync([FromQuery]long id, [FromQuery] int index) + { + var data = await JournalService.StartPageAsync(id, index); + return BaseResponse.Success(data); + } + + /// + /// 书籍归档 + /// + /// + [HttpPost, Route("Journal/rchive/{id:long}")] + public async Task> Archive(long id) + { + var data = await JournalService.StatusAsync(id, JournalStatusEnum.Archive); + return BaseResponse.Success(data); + } + + /// + /// 书籍废弃 + /// + /// + [HttpPost] + [HttpPost, Route("Journal/abandon/{id:long}")] + public async Task> Abandon(long id) + { + var data = await JournalService.StatusAsync(id, JournalStatusEnum.Abandoned); + return BaseResponse.Success(data); + } + + /// + /// 书籍铺码 + /// + /// + [HttpPost, Route("Journal/printcode/{id:long}")] + public async Task> PrintCodeAsync(long id) + { + var data = await JournalService.PrintCodeAsync(id); + return BaseResponse.Success(data); + } + /// + /// 上报铺码结果 + /// + /// + /// + /// + [HttpPost, Route("Journal/resultreport")] + public async Task> GenerateAsync([FromBody] DotMatrixNoteJournalReportInput input, [FromForm] IFormFile? file) + { + if (file != null) + { + input.FileKey = $"Journal/{input.Id}/dotMatrixJournal.pdf"; + input.FileUrl = ossService.PutObject(input.FileKey, file.OpenReadStream()); + } + var data = await JournalService.ResultReportAsync(input); + return BaseResponse.Success(data); + } + + /// + /// 书籍导入 + /// + /// + /// + [HttpPost, Route("Journal/import")] + public async Task> JournalImport(JournalImportDto input) + { + var data = await JournalCatalogService.ImportAsync(input); + return BaseResponse.Success(data); + } + + ///// + ///// 工单分页列表 + ///// + ///// + ///// + //[HttpPost, Route("tasklibrary/workorder/pagelist")] + //public async Task>> WorkOrderPageListAsync([FromBody] PageQueryModel search) + //{ + // var data = await JournalWorkOrderService.WorkOrderPageListAsync(search); + // return BaseResponse>.Success(data); + //} + + /// + /// 书籍发起工单 + /// + /// + /// + //[HttpPost, Route("tasklibrary/workorder/create")] + //public async Task> CreateWorkOrderAsync([FromBody] IcrWorkOrderCreateDto dto) + //{ + // var result = await JournalWorkOrderService.CreateWorkOrderAsync(dto); + // return result; + //} + + ///// + ///// 更新工单状态 + ///// + ///// + ///// + //[HttpPost, Route("tasklibrary/workorder/setstatus/{JournalId:long}")] + //public async Task> UpdateWorkOrderAsync(long JournalId) + //{ + // var result = await JournalWorkOrderService.UpdateWorkOrderAsync(JournalId); + // return BaseResponse.Success(result); + //} + + /// + /// 题库代理请求接口 + /// + /// + [HttpGet, Route("tasklibrary/proxyget")] + public async Task GetTaskLibraryData(string apiUrl = "") + { + var client = httpClientFactory.CreateClient(); + var response = await client.GetAsync(configuration["TaskLibraryApi"] + apiUrl); + // 确保请求成功(状态码 200) + response.EnsureSuccessStatusCode(); + string result = await response.Content.ReadAsStringAsync(); + return Content(result, "application/json"); + } + #endregion + + + #region 书籍目录 + + ///// + ///// 书籍目录详情 + ///// + ///// + //[HttpGet, Route("catalog/tree/{JournalId:long}")] + //public async Task>> CatalogDetailAsync(long JournalId) + //{ + // var data = await _JournalCatalogService.DetailAsync(JournalId); + // return BaseResponse>.Success(data); + //} + + /// + /// 书籍目录详情 + /// + /// + [HttpGet, Route("catalog/tree/{JournalId:long}")] + public async Task>> CatalogDetailAsync(long JournalId) + { + var data = await JournalCatalogService.GetJournalCataloTreeAsync(JournalId); + return BaseResponse>.Success(data); + } + + /// + /// 获取书分类及页码 + /// + /// + /// + [HttpGet, Route("catalog/list")] + public async Task>> GetJournalCatelogListAsync(long JournalId) + { + var data = await JournalCatalogService.GetJournalCatalogListAsync(JournalId); + return BaseResponse>.Success(data); + } + + /// + /// 下载书籍目录导入模板 + /// + /// + [HttpGet, Route("catalog/template")] + [AllowAnonymous] + public IActionResult ImportCatalogTemplateExcel() + { + var result = DownloadImportTemplate(new List() { }, "书籍目录导入模板"); + return ExportExcel(result.Item2, result.Item1); + } + + /// + /// 书籍目录导出 + /// + /// + /// + [HttpGet, Route("catalog/export/{JournalId}")] + public async Task CatalogExport(long JournalId) + { + var list = await JournalCatalogService.GetJournalCatalogListAsync(JournalId); + + var result = ExportExcelMini(list, "sheet1", "书籍目录导出"); + return ExportExcel(result.Item2, result.Item1); + } + + /// + /// 书籍目录导入 + /// + /// file + /// file + /// + [HttpPost, Route("catalog/import/{JournalId}")] + public async Task> ImportAsync(long JournalId, [FromForm(Name = "file")] IFormFile file) + { + List dto = new(); + using (var stream = file.OpenReadStream()) + { + dto = stream.Query().ToList(); + } + var result = await JournalCatalogService.ImportCatalogAsync(JournalId, dto); + return BaseResponse.Success(result); + } + + /// + /// 书籍目录新增 + /// + /// + /// + [HttpPost, Route("catalog/add")] + public async Task> CatalogAdd(JournalCatalogInput input) + { + var data = await JournalCatalogService.InsertAsync(input); + return BaseResponse.Success(data); + } + /// + /// 书籍目录修改 + /// + /// + /// + [HttpPost, Route("catalog/update")] + public async Task> CatalogUpdate(JournalCatalogUpdateInput input) + { + var data = await JournalCatalogService.UpdateAsync(input); + return BaseResponse.Success(data); + } + /// + /// 书籍目录删除 + /// + /// + /// + [HttpPost, Route("catalog/delete/{id:long}")] + public async Task> CatalogDelete(long id) + { + var data = await JournalCatalogService.DeleteAsync(id); + return BaseResponse.Success(data); + } + + ///// + ///// 书籍目录复制 + ///// + ///// + ///// + //[HttpPost, Route("catalog/copy")] + //[ProducesResponseType(typeof(BaseResponse), 200)] + //public async Task CatalogCopy(CopyInput input) + //{ + // var data = await _JournalCatalogServices.CopyAsync(input); + // return ApiResult(data, "书籍目录删除失败!"); + //} + + /// + /// 书籍目录移动 + /// + /// + /// + [HttpPost, Route("catalog/move")] + public async Task> CatalogMove(MoveInput input) + { + var data = await JournalCatalogService.MoveAsync(input); + return BaseResponse.Success(data); + } + + #endregion + + + #region 书页 + /// + /// 书页新增 + /// + /// + [HttpPost, Route("page/add")] + public async Task> PageAdd(JournalAddV2Input input) + { + var data = await JournalPageService.InsertAsync(input); + return BaseResponse.Success(data); + } + + /// + /// 书页修改 + /// + /// + [HttpPost, Route("page/update")] + public async Task> PageUpdate(PageLayoutInput input) + { + var data = await JournalPageService.UpdateAsync(input); + return BaseResponse.Success(data); + } + + /// + /// 自动铺码 + /// + /// + [HttpPost, Route("page/updatepageno/{JournalId:long}")] + public async Task> PageUpdatePageNo([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId) + { + var data = await JournalPageService.UpdatePageNoAsync(JournalId); + return BaseResponse.Success(data); + } + + /// + /// 下载书籍页码点阵码PDF文件名称 + /// + /// + [HttpGet, Route("page/downloadJournalpagePdf/{JournalId:long}")] + public async Task DownloadJournalPagePdf([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId) + { + var entity = await JournalService.GetFirstAsync(x => x.Id == JournalId); + if (entity == null) + throw new BusinessException("书籍不存在"); + + var downloadUrl = entity.DownloadJournalPagePdfName; + if (string.IsNullOrWhiteSpace(downloadUrl)) + throw new BusinessException("下载点阵码PDF文件为空,请先铺码"); + + var stream = await httpClientFactory.CreateClient().GetStreamAsync(downloadUrl); //创建文件流 + if (stream == null) + throw new BusinessException("下载点阵码PDF文件失败"); + + Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition"); + + return File(stream, "application/pdf", HttpUtility.UrlEncode(Path.GetFileName(downloadUrl))); + } + + + ///// + ///// 打印书页(全部) 暂时废弃这个方,直接使用 Adobe Reader X 软件打印就可以了 + ///// + ///// + //[HttpPost, Route("page/printJournalPage/{JournalId:long}")] + //public async Task> PrintJournalPage([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId) + //{ + // var data = await JournalPageService.PrintJournalPageAsync(JournalId); + // return BaseResponse.Success(data); + //} + + + ///// + ///// 书页删除 + ///// + ///// + //[HttpPost, Route("page/delete/{id:long}")] + //[ProducesResponseType(typeof(BaseResponse), 200)] + //public async Task PageDelete(long id) + //{ + // var data = await _JournalPageServices.DeleteAsync(id); + // return ApiResult(data, "更新书页失败!"); + //} + + /// + /// 书页详情 + /// + /// + [HttpGet, Route("page/detail/{id:long}")] + public async Task> PageDetail(long id) + { + var data = await JournalPageService.DetailAsync(id); + return BaseResponse.Success(data); + } + + ///// + ///// 不包含有文章的书页 + ///// + ///// + //[HttpGet, Route("page/no-article/{JournalId:long}")] + //[ProducesResponseType(typeof(BaseResponse>), 200)] + //public async Task PageNoArticleAsync(long JournalId) + //{ + // var data = await _JournalPageService.PageNoArticleAsync(JournalId); + // return BaseResponse>(data, "查询书页失败!"); + //} + + #endregion + + #region 书页问题 + /// + /// 书页问题新增 + /// + /// + [HttpPost, Route("page/task/add")] + [ProducesResponseType(typeof(BaseResponse), 200)] + public async Task> PageTaskAdd(JournalPageTaskAddInput input) + { + var data = await JournalPageTaskService.InsertAsync(input); + return BaseResponse.Success(data.Value); + } + + /// + /// 书页问题单项解析 + /// + /// + [HttpPost, Route("page/task/keyword-analysis")] + [ProducesResponseType(typeof(BaseResponse), 200)] + public async Task> KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input) + { + var data = await JournalPageTaskService.KeywordAnalysisAsync(input); + return BaseResponse.Success(data); + } + + /// + /// 书页问题更新 + /// + /// + [HttpPost, Route("page/task/update")] + [ProducesResponseType(typeof(BaseResponse), 200)] + public async Task> PageTaskUpdate(JournalPageTaskUpdateInput input) + { + var data = await JournalPageTaskService.UpdateAsync(input); + return BaseResponse.Success(data); + } + + /// + /// 书页问题详情 + 题库那边需要用 + /// + /// + [AllowAnonymous] + [HttpGet, Route("page/task/detail/{id:long}")] + public async Task> PageTaskDetail(long id) + { + var data = await JournalPageTaskService.DetailAsync(id); + return BaseResponse.Success(data); + } + + /// + /// 书页问题删除 + /// + /// + [HttpPost, Route("page/task/delete/{id:long}")] + public async Task> PageTaskDelete(long id) + { + var data = await JournalPageTaskService.DeleteAsync(id); + return BaseResponse.Success(data); + } + + /// + /// 书页问题补充 + /// + /// + [HttpPost, Route("page/task/complement")] + public async Task> ComplementAnsync(JournalPageTaskComplementInput input) + { + var data = await JournalPageTaskService.ComplementAsync(input); + return BaseResponse.Success(data); + } + #endregion + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Program.cs b/QYZH.InteractiveMagazine.WebApi/Program.cs index 2622f55..02d1683 100644 --- a/QYZH.InteractiveMagazine.WebApi/Program.cs +++ b/QYZH.InteractiveMagazine.WebApi/Program.cs @@ -25,6 +25,7 @@ using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerUI; using System.Text.Json.Serialization; using Yitter.IdGenerator; +using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ; var builder = WebApplication.CreateBuilder(args); @@ -51,6 +52,9 @@ builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection")); //redis builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings")); + +//MQ +builder.Services.AddRabbitMQ(builder.Configuration); // 配置Serilog Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.json b/QYZH.InteractiveMagazine.WebApi/appsettings.json index 8669a56..daf9305 100644 --- a/QYZH.InteractiveMagazine.WebApi/appsettings.json +++ b/QYZH.InteractiveMagazine.WebApi/appsettings.json @@ -13,12 +13,13 @@ "Sentinels": [], "ExpireSecondRange": [ 3600, 7200 ] }, - "RabbitMQSettings": { + "RabbitMq": { "HostName": "192.168.20.150", "Port": 5672, "UserName": "smartschool", "Password": "@ss%&*otz%d*pq2S", - "VirtualHost": "InteractiveMagazine" + "VirtualHost": "InteractiveMagazine", + "ClientProvidedName": "Custom connection name" }, "WeChatSettings": { "AppId": "wx7922cc9b6023f3ac",