refactor,feat: 批量代码重构与新增业务模块

1. 重命名枚举项与实体字段,修正类型引用
2. 新增IsNull扩展方法与多项字符串处理扩展
3. 新增大量业务DTO、服务接口与枚举定义
4. 重构RabbitMQ服务实现,替换旧版消息队列组件
5. 优化签到服务的宠物喂养事务逻辑
6. 移除冗余的项目引用与旧版消息队列代码
7. 新增Excel导出、导入模板相关工具方法
This commit is contained in:
glz
2026-06-11 17:01:24 +08:00
parent ae4cd627df
commit e493c85d08
57 changed files with 4477 additions and 399 deletions

View File

@ -0,0 +1,441 @@
using System.Collections;
namespace QYZH.InteractiveMagazine.Common.Extensions
{
public static partial class Extensions
{
#region long
/// <summary>
/// 将object转换为long若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static long ParseToLong(this object obj)
{
try
{
return long.Parse(obj.ToString());
}
catch
{
return 0L;
}
}
/// <summary>
/// 将object转换为long若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
public static long ParseToLong(this string str, long defaultValue)
{
try
{
return long.Parse(str);
}
catch
{
return defaultValue;
}
}
#endregion
#region int
/// <summary>
/// 将object转换为int若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static int ParseToInt(this object str)
{
try
{
return Convert.ToInt32(str);
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为int若转换失败则返回指定值。不抛出异常。
/// null返回默认值
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
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
/// <summary>
/// 将object转换为short若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static short ParseToShort(this object obj)
{
try
{
return short.Parse(obj.ToString());
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为short若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static short ParseToShort(this object str, short defaultValue)
{
try
{
return short.Parse(str.ToString());
}
catch
{
return defaultValue;
}
}
#endregion
#region demical
/// <summary>
/// 将object转换为demical若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static decimal ParseToDecimal(this object str, decimal defaultValue)
{
try
{
return decimal.Parse(str.ToString());
}
catch
{
return defaultValue;
}
}
/// <summary>
/// 将object转换为demical若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static decimal ParseToDecimal(this object str)
{
try
{
return decimal.Parse(str.ToString());
}
catch
{
return 0;
}
}
#endregion
#region bool
/// <summary>
/// 将object转换为bool若转换失败则返回false。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool ParseToBool(this object str)
{
try
{
return bool.Parse(str.ToString());
}
catch
{
return false;
}
}
/// <summary>
/// 将object转换为bool若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool ParseToBool(this object str, bool result)
{
try
{
return bool.Parse(str.ToString());
}
catch
{
return result;
}
}
#endregion
#region float
/// <summary>
/// 将object转换为float若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static float ParseToFloat(this object str)
{
try
{
return float.Parse(str.ToString());
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为float若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static float ParseToFloat(this object str, float result)
{
try
{
return float.Parse(str.ToString());
}
catch
{
return result;
}
}
#endregion
#region Guid
/// <summary>
/// 将string转换为Guid若转换失败则返回Guid.Empty。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static Guid ParseToGuid(this string str)
{
try
{
return new Guid(str);
}
catch
{
return Guid.Empty;
}
}
#endregion
#region DateTime
/// <summary>
/// 将string转换为DateTime若转换失败则返回日期最小值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// 将string转换为DateTime若转换失败则返回默认值。
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
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
/// <summary>
/// 将object转换为string若转换失败则返回""。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
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<T>(this object obj)
{
try
{
var list = obj as IEnumerable<T>;
if (list != null)
{
return string.Join(",", list);
}
else
{
return obj.ToString();
}
}
catch
{
return string.Empty;
}
}
#endregion
#region double
/// <summary>
/// 将object转换为double若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static double ParseToDouble(this object obj)
{
try
{
return double.Parse(obj.ToString());
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为double若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
public static double ParseToDouble(this object str, double defaultValue)
{
try
{
return double.Parse(str.ToString());
}
catch
{
return defaultValue;
}
}
#endregion
#region
/// <summary>
/// 强制转换类型
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<TResult> CastSuper<TResult>(this IEnumerable source)
{
foreach (object item in source)
{
yield return (TResult)Convert.ChangeType(item, typeof(TResult));
}
}
#endregion
}
}

View File

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

View File

@ -95,5 +95,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
{ {
return s == null || s?.Count() < 1; return s == null || s?.Count() < 1;
} }
public static bool IsNull(this object? s)
{
return s == null;
}
} }
} }

View File

@ -163,5 +163,55 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
// 匹配域名后的路径部分(包括第一个/ // 匹配域名后的路径部分(包括第一个/
return Regex.Replace(str, @"https?://[^/]+/([^""\s<>]*)", "$1"); 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<string> AllUrl(this string str, string url = null)
{
if (string.IsNullOrEmpty(str)) return new List<string>();
string pattern = @"src=""([^""]+)""";
var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase);
return matches
.Cast<Match>()
.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();
}
} }
} }

View File

@ -1,9 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />

View File

@ -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<JournalCatalog>
{
Task<List<JournalCatalog>> DetailAsync(long JournalId);
Task<long> InsertAsync(JournalCatalogInput input);
Task<long> ImportAsync(JournalImportDto input);
Task<bool> UpdateAsync(JournalCatalogUpdateInput input);
Task<bool> DeleteAsync(long id);
Task<bool> MoveAsync(MoveInput input);
//Task<bool> CopyAsync(CopyInput input);
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<List<JournalCatalogTreeListDto>> GetJournalCatalogListAsync(long JournalId);
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<List<IcrJournalCatalogTreeDto>> GetJournalCataloTreeAsync(long JournalId);
/// <summary>
/// 导入书籍目录
/// </summary>
/// <param name="JournalId"></param>
/// <param name="dtos"></param>
/// <returns></returns>
Task<bool> ImportCatalogAsync(long JournalId, List<JournalCatalogTreeListDto> dtos);
}

View File

@ -0,0 +1,32 @@

using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
public interface IJournalPageService: IBaseService<JournalPage>
{
Task<long> InsertAsync(JournalAddV2Input input);
Task<bool> UpdateAsync(PageLayoutInput input);
/// <summary>
/// 修改书页的点阵码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<bool> UpdatePageNoAsync(long JournalId);
/// <summary>
/// 打印书页(全部)
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<bool> PrintJournalPageAsync(long JournalId);
Task<JournalPageV2Output> DetailAsync(long id);
Task<bool> DeleteAsync(long id);
//Task<List<JournalPageNoArticleOutput>> PageNoArticleAsync(long id);
}

View File

@ -0,0 +1,17 @@

using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
public interface IJournalPageTaskService : IBaseService<JournalPageTask>
{
Task<long?> InsertAsync(JournalPageTaskAddInput input);
Task<bool> UpdateAsync(JournalPageTaskUpdateInput input);
Task<bool> KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input);
Task<JournalPageTaskOutput> DetailAsync(long id);
Task<bool> DeleteAsync(long id);
Task<bool> ComplementAsync(JournalPageTaskComplementInput input);
}

View File

@ -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<Journal>
{
/// <summary>
/// 查询List
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<List<JournalDto>> GetListAsync(JournalQueryDto dto);
/// <summary>
/// 分页查询
/// </summary>
/// <param name="search"></param>
/// <returns></returns>
Task<PageListModel<JournalDto>> GetPageListAsync(PageQueryModel<JournalQueryDto> search);
/// <summary>
/// 编辑
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<BaseResponse<bool>> EditAsync(JournalEditDto input);
Task<List<JournalTaskOutput>> Tasks(long id);
Task<JournalDto> DetailAsync(long id);
Task<bool> DeleteAsync(List<long> id);
Task<bool> StartPageAsync(long Id, int Index);
Task<bool> StatusAsync(long id, JournalStatusEnum status);
Task<DotMatrixOutput> PrintCodeAsync(long id);
Task<bool> ResultReportAsync(DotMatrixNoteJournalReportInput input);
}

View File

@ -37,6 +37,14 @@ public interface IPetService : IBaseService<UserPet>
/// <returns>喂养结果</returns> /// <returns>喂养结果</returns>
Task<FeedPetOutput> FeedPetAsync(long userId, FeedPetInput input); Task<FeedPetOutput> FeedPetAsync(long userId, FeedPetInput input);
/// <summary>
/// 喂养宠物(无事务,需在外部事务中调用)
/// </summary>
/// <param name="userId">用户Id</param>
/// <param name="input">喂养输入</param>
/// <returns>喂养结果</returns>
Task<FeedPetOutput> FeedPetInTranAsync(long userId, FeedPetInput input);
/// <summary> /// <summary>
/// 获取宠物喂养记录列表 /// 获取宠物喂养记录列表
/// </summary> /// </summary>

View File

@ -2,7 +2,6 @@
using Autofac.Extensions.DependencyInjection; using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
using QYZH.InteractiveMagazine.Models.Settings; using QYZH.InteractiveMagazine.Models.Settings;
using RabbitMQ.Client; using RabbitMQ.Client;
using StackExchange.Redis; using StackExchange.Redis;
@ -25,31 +24,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
var source = friendlyName.Split('.'); var source = friendlyName.Split('.');
var assemblyNames = string.Join(".", source.Take(source.Length - 1)); var assemblyNames = string.Join(".", source.Take(source.Length - 1));
containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames)); containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames));
InitializeRabbitMQ(c.Configuration, containerBuilder);
}); });
} }
private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder)
{
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>();
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<IConnection>().SingleInstance();
containerBuilder.RegisterType<RabbitMQPublisher>().InstancePerDependency();
}
}
} }
} }

View File

@ -1,72 +0,0 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
/// <summary>
/// RabbitMQ消息消费者基类
/// </summary>
public abstract class RabbitMQConsumer : IDisposable
{
private readonly IConnection _connection;
private readonly ILogger<RabbitMQConsumer> _logger;
private IChannel? _channel;
private AsyncEventingBasicConsumer? _consumer;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connection">RabbitMQ连接</param>
/// <param name="logger">日志记录器</param>
protected RabbitMQConsumer(IConnection connection, ILogger<RabbitMQConsumer> logger)
{
_connection = connection;
_logger = logger;
}
/// <summary>
/// 启动消费
/// </summary>
/// <param name="queueName">队列名称</param>
/// <param name="handleMessage">消息处理委托</param>
public async Task StartConsume(string queueName, Func<string, Task> 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);
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_channel?.DisposeAsync().GetAwaiter().GetResult();
GC.SuppressFinalize(this);
}
}

View File

@ -1,47 +0,0 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
/// <summary>
/// RabbitMQ消息发布器
/// </summary>
public class RabbitMQPublisher
{
private readonly IConnection _connection;
private readonly ILogger<RabbitMQPublisher> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connection">RabbitMQ连接</param>
/// <param name="logger">日志记录器</param>
public RabbitMQPublisher(IConnection connection, ILogger<RabbitMQPublisher> logger)
{
_connection = connection;
_logger = logger;
}
/// <summary>
/// 发布消息
/// </summary>
/// <param name="exchange">交换机名称</param>
/// <param name="routingKey">路由键</param>
/// <param name="message">消息内容</param>
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);
}
}

View File

@ -217,7 +217,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.OSS
//var del = _ossResourceRepository.Delete(w => keys.Contains(w.Path)); //var del = _ossResourceRepository.Delete(w => keys.Contains(w.Path));
//AppException.ThrowIf(!del, "删除资源失败"); //BusinessException.ThrowIf(!del, "删除资源失败");
return result.Keys.Count() == keys.Count; return result.Keys.Count() == keys.Count;
@ -286,7 +286,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.OSS
// Path = targetObject, // Path = targetObject,
//}); //});
//AppException.ThrowIf(data.IsNull(), "资源添加失败"); //BusinessException.ThrowIf(data.IsNull(), "资源添加失败");
var req = new CopyObjectRequest(_ossOption.BucketName, sourceObject, _ossOption.BucketName, targetObject) var req = new CopyObjectRequest(_ossOption.BucketName, sourceObject, _ossOption.BucketName, targetObject)
{ {

View File

@ -0,0 +1,14 @@
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public interface IRabbitMQService
{
Task<bool> SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default);
Task<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @params, CancellationToken cancellationToken = default);
Task ReceiveAsync(string queueName, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,62 @@
using RabbitMQ.Client;
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public interface IRabbitMQConnection : IDisposable
{
Task<IChannel> 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<IChannel> 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));
}
}
}
}

View File

@ -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; } = "/";
}
}

View File

@ -0,0 +1,25 @@
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public class RabbitMQSendParam
{
/// <summary>
/// 交换机 默认空
/// </summary>
public string Exchange { get; set; } = "";
public string Queue { get; set; }
/// <summary>
/// 路由键
/// </summary>
public string RoutingKey { get; set; }
/// <summary>
/// 消息数据
/// </summary>
public object Data { get; set; }
/// <summary>
/// 是否清空队列
/// </summary>
public bool Purge { get; set; } = false;
}
}

View File

@ -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<bool> 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<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @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<string>();
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<IChannel, BasicDeliverEventArgs, Task> 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);
}
}
}

View File

@ -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
{
/// <summary>
/// 初始化消息队列并添加Publisher到IoC容器
/// </summary>
/// <remarks>从Configuration读取"RabbbitMQOptions配置项"</remarks>
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, IConfiguration configuration)
{
var rabbitMqSection = configuration.GetSection("RabbitMq");
if (rabbitMqSection.Exists())
{
// 绑定RabbitMQ配置
services.Configure<RabbitMQOptions>(rabbitMqSection);
// 注册RabbitMQ连接工厂
services.AddSingleton<IRabbitMQConnection, RabbitMQConnection>(sp =>
{
var options = sp.GetRequiredService<IOptions<RabbitMQOptions>>().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<IRabbitMQService, RabbitMQService>();
//services.AddHostedService<TerminalReportService>();
}
return services;
}
}
}

View File

@ -43,4 +43,23 @@ public class BusinessException : Exception
public BusinessException(string message, Exception innerException) : base(message, innerException) 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<string> 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);
}
}
} }

View File

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

View File

@ -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
{
/// <summary>
/// 方法
/// </summary>
public string Method { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// id
/// </summary>
public long NoteId { get; set; }
private string _pdfUrl;
/// <summary>
/// pdf地址
/// </summary>
public string PdfUrl
{
get => DomainHelper.OssFullUrl(_pdfUrl);
set => _pdfUrl = value;
}
public List<DotMatrixMqOutput> DotMatrixs { get; set; } = new List<DotMatrixMqOutput>();
}
public class DotMatrixMqOutput
{
public long DotId { get; set; }
private string _fileAddress;
/// <summary>
/// 点阵xml地址
/// </summary>
public string FileAddress
{
get => DomainHelper.OssFullUrl(_fileAddress);
set => _fileAddress = value;
}
/// <summary>
/// 页
/// </summary>
public List<DotMatrixPageDto> Pages { get; set; } = new List<DotMatrixPageDto>();
}
public class DotMatrixPageDto
{
/// <summary>
/// 页码
/// </summary>
public string PageNo { get; set; }
/// <summary>
/// 连续数量
/// </summary>
public int PageNum { get; set; }
}
}

View File

@ -0,0 +1,81 @@
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
/// 绑定期刊输入DTO
/// </summary>
public class BindJournalInput
{
/// <summary>
/// 期刊模板Id扫码解析的期刊定义Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 实例化期刊Id扫码解析的具体期刊实例Id可选
/// </summary>
public long Id { get; set; }
/// <summary>
/// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe
/// </summary>
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString();
}
/// <summary>
/// 绑定期刊输出DTO
/// </summary>
public class BindJournalOutput
{
/// <summary>
/// 绑定记录Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 用户Id
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 期刊模板Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 关联类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 绑定时间
/// </summary>
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// 用户期刊关联查询输入DTO
/// </summary>
public class UserJournalQueryInput : PageQueryModel
{
/// <summary>
/// 期刊模板Id
/// </summary>
public long? JournalId { get; set; }
/// <summary>
/// 实例化期刊Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 关联类型: Read, Favorite, Subscribe
/// </summary>
public string? Type { get; set; }
}

View File

@ -0,0 +1,112 @@
using System;
using System.Linq;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalImportDto
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// pdf地址
/// </summary>
public string PdfUrl { get; set; }
/// <summary>
/// 从多少页开始
/// </summary>
public int Index { get; set; }
public List<JournalCatalogAddInput> JournalCatalogs { get; set; }
}
/// <summary>
/// 目录导入
/// </summary>
public class JournalCatalogImportDto
{
/// <summary>
/// 一级目录
/// </summary>
public string ParentName { get; set; }
/// <summary>
/// 二级目录
/// </summary>
public string Name { get; set; }
/// <summary>
/// 页码
/// </summary>
public int PageNum { get; set; }
[JsonIgnore]
public long PagePageId { get; set; }
}
public class JournalCatalogAddInput
{
/// <summary>
/// 目录名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 级别
/// </summary>
public int Level { get; set; }
/// <summary>
/// 类型0-目录 1-页
/// </summary>
public int Type { get; set; }
/// <summary>
/// url
/// </summary>
public string Url { get; set; }
}
public class JournalCatalogInput
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 目录名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 父Id;无限级别
/// </summary>
public long ParentId { get; set; }
/// <summary>
/// 等级
/// </summary>
public int Level { get; set; }
/// <summary>
/// 指定插入位置的索引从0开始不传或传null时默认追加到末尾
/// </summary>
public int? Position { get; set; }
/// <summary>
/// 类型0-目录 1-页
/// </summary>
public int Type { get; set; }
/// <summary>
/// url
/// </summary>
public string Url { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Desc:主键id
/// </summary>
[ExcelIgnore]
public long Id { get; set; }
/// <summary>
/// Desc:书id
/// </summary>
[ExcelIgnore]
public long JournalId { get; set; }
/// <summary>
/// Desc:一级目录名称
/// </summary>
[DisplayName("目录名称")]
[Required]
public string? ParentName { get; set; }
/// <summary>
/// Desc:目录名称
/// </summary>
[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
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 层级
/// </summary>
public int Level { get; set; }
public List<IcrJournalCatalogTreeDto> Child { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Linq;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalCatalogUpdateInput
{
public long Id { get; set; }
/// <summary>
/// 目录名称
/// </summary>
public string Name { get; set; }
}
}

View File

@ -1,81 +1,145 @@
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Common.Helpers;
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; namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// <summary>
/// 绑定期刊输入DTO
/// </summary>
public class BindJournalInput
{ {
/// <summary> public class JournalDto
/// 期刊模板Id扫码解析的期刊定义Id {
/// </summary> /// <summary>
public long JournalId { get; set; } /// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary> /// <summary>
/// 实例化期刊Id扫码解析的具体期刊实例Id可选 /// Desc:书籍名称
/// </summary> /// </summary>
public long Id { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe /// Desc:总页数
/// </summary> /// </summary>
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString(); public int TotalPage { get; set; }
}
/// <summary>
/// <summary> /// Desc:校验页数
/// 绑定期刊输出DTO /// </summary>
/// </summary> public int VerifyPage { get; set; }
public class BindJournalOutput
{
/// <summary>
/// 绑定记录Id /// <summary>
/// </summary> /// Desc:状态
public long Id { get; set; } /// </summary>
public JournalStatusEnum Status { get; set; }
/// <summary>
/// 用户Id /// <summary>
/// </summary> /// 状态
public long UserId { get; set; } /// </summary>
public string ShowStatus => Status.GetDescription();
/// <summary>
/// 期刊模板Id private string _pdfUrl;
/// </summary> /// <summary>
public long JournalId { get; set; } /// pdf预览地址
/// </summary>
/// <summary> public string PdfUrl
/// 关联类型 {
/// </summary> get => DomainHelper.OssFullUrl(_pdfUrl);
public string Type { get; set; } = string.Empty; set => _pdfUrl = value;
}
/// <summary>
/// 状态 /// <summary>
/// </summary> /// Desc:宽度
public string Status { get; set; } = string.Empty; /// </summary>
public float Width { get; set; }
/// <summary>
/// 绑定时间 /// <summary>
/// </summary> /// Desc:高度
public DateTime CreatedAt { get; set; } /// </summary>
} public float Height { get; set; }
/// <summary> /// <summary>
/// 用户期刊关联查询输入DTO /// Desc:乐观锁
/// </summary> /// </summary>
public class UserJournalQueryInput : PageQueryModel public int Revision { get; set; }
{
/// <summary> /// <summary>
/// 期刊模板Id /// Desc:审查结果(默认0 1通过 -1驳回)
/// </summary> /// </summary>
public long? JournalId { get; set; } public int Result { get; set; }
/// <summary> private string _cover;
/// 实例化期刊Id /// <summary>
/// </summary> /// 封面
public long Id { get; set; } /// </summary>
public string Cover
/// <summary> {
/// 关联类型: Read, Favorite, Subscribe get => DomainHelper.OssFullUrl(_cover);
/// </summary> set => _cover = value;
public string? Type { get; set; } }
private string _backCover;
/// <summary>
/// 封底
/// </summary>
public string BackCover
{
get => DomainHelper.OssFullUrl(_backCover);
set => _backCover = value;
}
private string _pdfPreviewUrl;
/// <summary>
/// pdf预览地址
/// </summary>
public string PdfPreviewUrl
{
get => DomainHelper.OssFullUrl(_pdfPreviewUrl);
set => _pdfPreviewUrl = value;
}
/// <summary>
/// Desc:结论json
/// </summary>
public object? Conclusion { get; set; }
/// <summary>
/// Desc:思路导读
/// </summary>
public string? Guide { get; set; }
/// <summary>
/// Desc:家长指导
/// </summary>
public string? Tutelage { get; set; }
/// <summary>
/// Desc:所属机构列表
/// </summary>
public Dictionary<long, string>? Organization { get; set; }
//public List<Dictionary<long, string>> Organizations => Organization.Select(c => new Dictionary<long, string>() { { c.Key, c.Value } }).ToList();
/// <summary>
/// Desc:副标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 年级
/// </summary>
public int Grade { get; set; }
/// <summary>
/// Desc:下载书籍页码点阵码PDF文件名称
/// </summary>
public string? DownloadJournalPagePdfName { get; set; }
}
} }

View File

@ -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
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// Desc:书籍名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:总页数
/// </summary>
public int TotalPage { get; set; }
/// <summary>
/// Desc:校验页数
/// </summary>
public int VerifyPage { get; set; }
/// <summary>
/// Desc:状态
/// </summary>
public JournalStatusEnum Status { get; set; } = JournalStatusEnum.Created;
/// <summary>
/// Desc:pdf地址
/// </summary>
public string PdfUrl { get; set; }
/// <summary>
/// Desc:宽度
/// </summary>
public float Width { get; set; }
/// <summary>
/// Desc:高度
/// </summary>
public float Height { get; set; }
/// <summary>
/// Desc:乐观锁
/// </summary>
[JsonIgnore]
public int Revision { get; set; }
/// <summary>
/// Desc:审查结果(默认0 1通过 -1驳回)
/// </summary>
public int Result { get; set; }
/// <summary>
/// Desc:封面
/// </summary>
public string Cover { get; set; }
/// <summary>
/// Desc:封底
/// </summary>
public string BackCover { get; set; }
/// <summary>
/// Desc:pdf预览地址
/// </summary>
public string PdfPreviewUrl { get; set; }
/// <summary>
/// Desc:结论json
/// </summary>
public object Conclusion { get; set; }
/// <summary>
/// Desc:思路导读
/// </summary>
public string Guide { get; set; }
/// <summary>
/// Desc:家长指导
/// </summary>
[JsonIgnore]
public string Tutelage { get; set; }
/// <summary>
/// Desc:所属机构
/// </summary>
public long[] OrganizationIds { get; set; }
///// <summary>
///// Desc:所属机构
///// </summary>
//public string OrganizationName { get; set; }
/// <summary>
/// Desc:副标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 年级
/// </summary>
public int Grade { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Linq;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalAddV2Input
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书目录id
/// </summary>
public long JournalCatalogId { get; set; }
/// <summary>
/// 页码
/// </summary>
public int PageNum { get; set; }
/// <summary>
/// 页图片
/// </summary>
public string Url { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// 学科Id
/// </summary>
public long SubjectId { get; set; }
/// <summary>
/// 分数
/// </summary>
public float Score { get; set; }
/// <summary>
/// 回答时间(秒)
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// 关联的知识点ids
/// </summary>
public string KnowledgePointIds { get; set; }
/// <summary>
/// 关键解析
/// </summary>
public string KeywordAnalysis { get; set; }
/// <summary>
/// 问题内容
/// </summary>
public string Task { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// 问题Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 单项解析
/// </summary>
public string KeywordAnalysis { get; set; }
}
}

View File

@ -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; }
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页Id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 学科Id
/// </summary>
public long SubjectId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
public string ShowType => Type.GetDescription();
private string _task;
/// <summary>
/// 问题
/// </summary>
public string Task
{
get { return _task.AddDomain(DomainHelper.OssDomain); }
set { _task = value; }
}
/// <summary>
/// 选项
/// </summary>
public string Options { get; set; }
private string _answer;
/// <summary>
/// 答案
/// </summary>
public string Answer
{
get { return _answer.AddDomain(DomainHelper.OssDomain); }
set { _answer = value; }
}
private string _analysis;
/// <summary>
/// 解析
/// </summary>
public string Analysis
{
get { return _analysis.AddDomain(DomainHelper.OssDomain); }
set { _analysis = value; }
}
public string AnalysisUrl { get; set; }
/// <summary>
/// 问题分数
/// </summary>
public float TaskScore { get; set; }
private string _audioUrl;
/// <summary>
/// 音频地址
/// </summary>
public string AudioUrl
{
get { return DomainHelper.OssFullUrl(_audioUrl); }
set { _audioUrl = value; }
}
private string _videoUrl;
/// <summary>
/// 视频地址
/// </summary>
public string VideoUrl
{
get { return DomainHelper.OssFullUrl(_videoUrl); }
set { _videoUrl = value; }
}
private string _pointsUrl;
/// <summary>
/// 点位数据地址
/// </summary>
public string PointsUrl
{
get { return DomainHelper.OssFullUrl(_pointsUrl); }
set { _pointsUrl = value; }
}
private string _imageUrl;
/// <summary>
/// 图片地址
/// </summary>
public string ImageUrl
{
get { return DomainHelper.OssFullUrl(_imageUrl); }
set { _imageUrl = value; }
}
public List<string> ImageUrls { get; set; }
/// <summary>
/// 问题分数
/// </summary>
public float Score { get; set; }
/// <summary>
/// 回答时间(秒)
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// 音频开始时间
/// </summary>
public long AudioStartTime { get; set; }
/// <summary>
/// 音频结束时间
/// </summary>
public long AudioEndTime { get; set; }
/// <summary>
/// 知识点
/// </summary>
public string KnowledgePointIds { get; set; }
/// <summary>
/// 回答项解析
/// </summary>
public string KeywordAnalysis { get; set; }
/// <summary>
/// 年级
/// </summary>
public int Grade { get; set; }
}
}

View File

@ -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; }
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// 问题
/// </summary>
public string Task { get; set; }
/// <summary>
/// 选项
/// </summary>
public string Options { get; set; }
/// <summary>
/// 答案
/// </summary>
public string Answer { get; set; }
/// <summary>
/// 答案图片
/// </summary>
public string AnswerUrl { get; set; }
/// <summary>
/// 解析
/// </summary>
public string Analysis { get; set; }
/// <summary>
/// 解析图片地址
/// </summary>
public string AnalysisUrl { get; set; }
/// <summary>
/// 问题分数
/// </summary>
public float Score { get; set; }
/// <summary>
/// 回答时间秒
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// 图片
/// </summary>
public string ImageUrl { get; set; }
/// <summary>
/// 视频
/// </summary>
public string VideoUrl { get; set; }
/// <summary>
/// 学科Id
/// </summary>
public long SubjectId { get; set; }
/// <summary>
/// 关联的知识点ids
/// </summary>
public string KnowledgepointIds { get; set; }
}
public class JournalPageTaskComplementInput
{
public long Id { get; set; }
/// <summary>
/// 任务id
/// </summary>
public long AssignTaskId { get; set; }
/// <summary>
/// 音频地址
/// </summary>
public string AudioUrl { get; set; }
/// <summary>
/// 音频开始时间
/// </summary>
public long AudioStartTime { get; set; }
/// <summary>
/// 音频结束时间
/// </summary>
public long AudioEndTime { get; set; }
/// <summary>
/// 点位数据地址
/// </summary>
public string PointsUrl { get; set; }
}
}

View File

@ -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; }
/// <summary>
/// 页码
/// </summary>
public int PageNum { get; set; }
private string _url;
/// <summary>
/// 图片
/// </summary>
public string Url
{
get { return DomainHelper.OssFullUrl(_url); }
set { _url = value; }
}
/// <summary>
/// 问题
/// </summary>
public List<JournalPageTaskV2Output>? Tasks { get; set; }
public List<JournalPageOtherOutput>? Areas { get; set; }
}
public class JournalPageTaskV2Output
{
public long Id { get; set; }
public long GroupId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
//public long test => long.Parse(No.Replace("-", string.Empty));
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
public string ShowType => Type.GetDescription();
/// <summary>
/// 选项
/// </summary>
public string Options { get; set; }
/// <summary>
/// 答案
/// </summary>
public string Answers { get; set; }
/// <summary>
/// 解析
/// </summary>
public string Analysis { get; set; }
/// <summary>
/// 是否分配
/// </summary>
public bool Assign { get; set; }
}
public class JournalPageOtherOutput
{
public long Id { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
public string ShowType => Type.GetDescription();
}
}

View File

@ -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
{
/// <summary>
/// 书籍名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 状态
/// </summary>
public JournalStatusEnum? Status { get; set; }
}
}

View File

@ -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}");
}
}
/// <summary>
/// 详细类型
/// </summary>
public TaskBankTypeEnum TaskSubType { get; set; }
}
}

View File

@ -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> TasksImages { get; set; }
}
public class TasksImages
{
public long TaskId { get; set; }
public string Url { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// 要移动节点ID (必须)
/// </summary>
[Required]
public long SourceId { get; set; }
/// <summary>
/// 目标父节点ID (0表示移动到根节点)
/// </summary>
public long TargetParentId { get; set; }
/// <summary>
/// 在目标父节点下的插入位置 (可选从0开始,null表示追加到末尾)
/// </summary>
[Range(0, int.MaxValue)]
public int? Position { get; set; }
}
}

View File

@ -14,73 +14,107 @@ namespace QYZH.InteractiveMagazine.Models.Entity
} }
/// <summary> /// <summary>
/// Desc:期刊号 /// Desc:书籍名称
/// Default:
/// Nullable:True
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:总页数
/// Default: /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
public int IssueNumber {get;set;} public int TotalPage { get; set; }
/// <summary> /// <summary>
/// Desc:期刊标题 /// Desc:校验页数
/// Default: /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
public string Title {get;set;} public int VerifyPage { get; set; }
/// <summary>
/// Desc:封面图地址
/// Default:
/// Nullable:True
/// </summary>
public string CoverImageUrl {get;set;}
/// <summary> /// <summary>
/// Desc:摘要 /// Desc:pdf地址
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
public string Summary {get;set;} public string PdfUrl { get; set; }
/// <summary> /// <summary>
/// Desc:发布时间 /// Desc:宽度
/// Default: /// Default:
/// Nullable:True /// Nullable:False
/// </summary> /// </summary>
public DateTime? PublishDate {get;set;} public float Width { get; set; }
/// <summary> /// <summary>
/// Desc:是否启用 /// Desc:高度
/// Default:b'1' /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
public bool IsActive {get;set;} public float Height { get; set; }
/// <summary> /// <summary>
/// Desc:排序权重 /// Desc:乐观锁
/// Default:0 /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
public int SortOrder {get;set;} public int Revision { get; set; }
/// <summary> /// <summary>
/// Desc:编者寄语 /// Desc:审查结果(默认0 1通过 -1驳回)
/// Default: /// Default:0
/// Nullable:True /// Nullable:False
/// </summary> /// </summary>
public string EditorNote {get;set;} public int Result { get; set; }
/// <summary> /// <summary>
/// Desc:主题色 /// Desc:封面
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
public string ThemeColor {get;set;} public string Cover { get; set; }
/// <summary>
/// Desc:封底
/// Default:
/// Nullable:True
/// </summary>
public string BackCover { get; set; }
/// <summary>
/// Desc:pdf预览地址
/// Default:
/// Nullable:True
/// </summary>
public string PdfPreviewUrl { get; set; }
public string Title { get; set; }
/// <summary>
/// Desc:下载书籍页码点阵码PDF文件名称
/// Default:
/// Nullable:True
/// </summary>
public string DownloadJournalPagePdfName { get; set; }
/// <summary>
/// Desc:书籍描述/简介
/// Default:
/// Nullable:True
/// </summary>
public string Description { get; set; }
/// <summary>
/// Desc:期刊类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public JournalTypeEnum Type {get;set;}
/// <summary>
/// Desc:期刊类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public JournalTypeEnum Type {get;set;}
} }
} }

View File

@ -43,7 +43,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Default: /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
public JournalPageTaskTypeEnum Type {get;set;} public TaskBankTypeEnum Type {get;set;}
/// <summary> /// <summary>
/// Desc:任务 /// Desc:任务

View File

@ -26,5 +26,5 @@ public enum JournalPageTaskTypeEnum
/// 问答题 /// 问答题
/// </summary> /// </summary>
[Description("问答题")] [Description("问答题")]
QuestionAnswer = 4 TaskAnswer = 4
} }

View File

@ -8,20 +8,58 @@ namespace QYZH.InteractiveMagazine.Models.Enum;
public enum JournalStatusEnum public enum JournalStatusEnum
{ {
/// <summary> /// <summary>
/// 草稿 /// 缺失目录
/// </summary> /// </summary>
[Description("草稿")] [Description("缺失目录")]
Draft = 0, MissCatalog = -1,
/// <summary>
/// 已创建
/// </summary>
[Description("已创建")]
Created = 0,
[Description("编辑")]
Editor = 1,
/// <summary> /// <summary>
/// 已发布 /// 已校验
/// </summary> /// </summary>
[Description("已发布")] [Description("已校验")]
Published = 1, Verify = 3,
/// <summary>
/// 铺码中
/// </summary>
[Description("铺码中")]
Codeing = 4,
/// <summary>
/// 铺码成功
/// </summary>
[Description("铺码成功")]
CodeSuccess = 5,
/// <summary>
/// 铺码失败
/// </summary>
[Description("铺码失败")]
CodeFail = -5,
/// <summary> /// <summary>
/// 已归档 /// 已归档
/// </summary> /// </summary>
[Description("已归档")] [Description("已归档")]
Archived = 2 Archive = 9,
/// <summary>
/// 已废弃
/// </summary>
[Description("已废弃")]
Abandoned = -9,
/// <summary>
/// 已发布
/// </summary>
[Description("已发布")]
Published = 999
} }

View File

@ -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
{
/// <summary>
/// 无效题目
/// </summary>
[Description("无效")]
Default = 0,
/// <summary>
/// 单选题
/// </summary>
[Description("单选题")]
Single = 1,
/// <summary>
/// 多选题
/// </summary>
[Description("多选题")]
Multiple = 2,
/// <summary>
/// 判断题
/// </summary>
[Description("判断题")]
Whether = 3,
/// <summary>
/// 填空题
/// </summary>
[Description("填空题")]
FillIn = 4,
/// <summary>
/// 应用题
/// </summary>
[Description("应用题")]
Problem = 5,
/// <summary>
/// 简答题
/// </summary>
[Description("简答题")]
ShortAnswer = 6,
}
public class TaskBankType
{
/// <summary>
/// 客观题
/// </summary>
public TaskBankTypeEnum[] ObjectiveTypes => new[] { TaskBankTypeEnum.Single, TaskBankTypeEnum.Multiple, TaskBankTypeEnum.Whether };
/// <summary>
/// 获取客观题类型的整数值
/// </summary>
public List<int> ObjectiveTypeValues => ObjectiveTypes.Select(t => (int)t).ToList();
/// <summary>
/// 主观题
/// </summary>
public TaskBankTypeEnum[] SubjectiveTypes => new[] { TaskBankTypeEnum.FillIn, TaskBankTypeEnum.Problem, TaskBankTypeEnum.ShortAnswer };
/// <summary>
/// 获取主观题类型的整数值
/// </summary>
public List<int> SubjectiveTypeValues => SubjectiveTypes.Select(t => (int)t).ToList();
}
}

View File

@ -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
{
/// <summary>
/// 客观题
/// </summary>
[Description("客观题")]
Objective = 1,
/// <summary>
/// 主观题
/// </summary>
[Description("主观题")]
Subjective = 2
}
}

View File

@ -9,9 +9,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="MiniExcel" Version="1.41.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.166" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.166" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" /> <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -68,7 +68,7 @@ public class CheckInService(
.Where(p => p.UserId == userId && !p.IsDeleted) .Where(p => p.UserId == userId && !p.IsDeleted)
.FirstAsync(); .FirstAsync();
// 6. 事务执行签到相关写操作 // 6. 事务执行签到相关写操作(含宠物喂养,统一事务)
var result = new CheckInOutput(); var result = new CheckInOutput();
await checkInRecordRepository.UseTranAsync(async () => await checkInRecordRepository.UseTranAsync(async () =>
@ -107,6 +107,17 @@ public class CheckInService(
Description = $"签到奖励(连续{consecutiveDays}天)" 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.RecordId = (long)recordId;
result.CheckInDate = today; result.CheckInDate = today;
@ -116,41 +127,17 @@ public class CheckInService(
result.PointsBalance = pointsResult.NewBalance; result.PointsBalance = pointsResult.NewBalance;
result.GrowthPointsBalance = newGrowthBalance; result.GrowthPointsBalance = newGrowthBalance;
result.HasPet = pet != null; result.HasPet = pet != null;
}); if (feedResult != null)
// 7. 如果用户有活跃宠物,调用 PetService 喂养(含进化检查),独立事务
if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{
try
{ {
var feedResult = await petService.FeedPetAsync(userId, new FeedPetInput
{
PetId = pet.Id,
GrowthPoints = growthReward
});
result.HasEvolved = feedResult.HasEvolved; result.HasEvolved = feedResult.HasEvolved;
result.EvolvedStageName = feedResult.EvolvedStageName; 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 if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{ {
TaskType = CompensationTaskTypeEnum.PetFeeding, logger.LogInformation("签到成长值已喂养宠物PetId: {PetId}, 进化: {HasEvolved}",
BusinessSource = "CheckIn", pet.Id, result.HasEvolved);
BusinessId = result.RecordId.ToString(),
UserId = userId,
Payload = new { PetId = pet.Id, GrowthPoints = growthReward },
ErrorMessage = ex.Message,
ErrorSource = "CheckInService.CheckInAsync → PetService.FeedPetAsync",
MaxRetries = 3
});
}
} }
logger.LogInformation("用户签到成功UserId: {UserId}, 连续{Days}天, 积分+{Points}, 成长值+{Growth}", logger.LogInformation("用户签到成功UserId: {UserId}, 连续{Days}天, 积分+{Points}, 成长值+{Growth}",
@ -339,6 +326,17 @@ public class CheckInService(
.ExecuteCommandAsync(); .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.RecordId = (long)recordId;
result.CheckInDate = targetDate; result.CheckInDate = targetDate;
result.ConsecutiveDays = 0; result.ConsecutiveDays = 0;
@ -347,27 +345,17 @@ public class CheckInService(
result.PointsBalance = pointsResult.NewBalance; result.PointsBalance = pointsResult.NewBalance;
result.GrowthPointsBalance = newGrowthBalance; result.GrowthPointsBalance = newGrowthBalance;
result.HasPet = pet != null; result.HasPet = pet != null;
}); if (feedResult != null)
// 如果有活跃宠物,喂养成长值
if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{
try
{ {
var feedResult = await petService.FeedPetAsync(userId, new Models.Dto.Pet.FeedPetInput
{
PetId = pet.Id,
GrowthPoints = growthReward
});
result.HasEvolved = feedResult.HasEvolved; result.HasEvolved = feedResult.HasEvolved;
result.EvolvedStageName = feedResult.EvolvedStageName; 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}", logger.LogInformation("补签成功UserId: {UserId}, Date: {Date}, 积分+{Points}, 成长值+{Growth}",

View File

@ -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<JournalCatalog>, IJournalCatalogService
{
private readonly BaseRepository<JournalPage> _JournalPageRepository;
private readonly BaseRepository<JournalPageTask> _JournalPageTaskRepository;
private readonly OssService _ossService;
public JournalCatalogService(BaseRepository<JournalPage> JournalPageRepository, BaseRepository<JournalPageTask> JournalPageTaskRepository, OssService ossService)
{
//_JournalRepository = JournalRepository;
_JournalPageRepository = JournalPageRepository;
_ossService = ossService;
}
public async Task<List<JournalCatalog>> 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<long> ImportAsync(JournalImportDto input)
{
var Journals = await Queryable<Journal>().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<Journal>().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;
}
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
public async Task<List<JournalCatalogTreeListDto>> GetJournalCatalogListAsync(long JournalId)
{
var Journal = await Queryable<Journal>().Where(w => w.Id == JournalId).FirstAsync();
BusinessException.ThrowIf(Journal.IsNull(), "未找到书本");
var pageNumList = await _JournalPageRepository.Queryable()
.InnerJoin<JournalCatalog>((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<JournalCatalog>().Where(c => c.Id == b.ParentId).Select(c => c.Name),
ParentId = b.ParentId,
PageId = a.Id,
})
.ToListAsync();
return pageNumList;
}
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
public async Task<List<IcrJournalCatalogTreeDto>> GetJournalCataloTreeAsync(long JournalId)
{
var Journal = await Queryable<Journal>().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<IcrJournalCatalogTreeDto>();
// 再查询有页面对应的目录及页面信息
var pageNumList = await _JournalPageRepository.Queryable()
.InnerJoin<JournalCatalog>((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<JournalCatalog>().Where(c => c.Id == b.ParentId).Select(c => c.Name),
ParentId = b.ParentId,
PageId = a.Id,
})
.ToListAsync();
var tree = BuildCatalogTree(allCatalogs, pageNumList);
return tree;
}
/// <summary>
/// 构建目录树形结构(支持任意层级)
/// </summary>
/// <param name="allCatalogs">所有目录</param>
/// <param name="pageNumList">有页面对应的目录信息</param>
/// <returns></returns>
private List<IcrJournalCatalogTreeDto> BuildCatalogTree(List<JournalCatalog> allCatalogs, List<JournalCatalogTreeListDto> 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<IcrJournalCatalogTreeDto>()
}).ToList()
);
// 构建所有目录节点字典
var catalogDict = new Dictionary<long, IcrJournalCatalogTreeDto>();
var rootCatalogIds = new HashSet<long>();
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<IcrJournalCatalogTreeDto>())
};
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;
}
/// <summary>
/// 递归计算子节点层级
/// </summary>
private void CalculateChildLevels(List<IcrJournalCatalogTreeDto> children, int level)
{
foreach (var child in children)
{
child.Level = level;
if (child.Child != null && child.Child.Count > 0)
{
CalculateChildLevels(child.Child, level + 1);
}
}
}
/// <summary>
/// 导入书籍目录
/// </summary>
/// <param name="JournalId"></param>
/// <param name="dtos"></param>
/// <returns></returns>
public async Task<bool> ImportCatalogAsync(long JournalId, List<JournalCatalogTreeListDto> 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<Journal>().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<List<JournalCatalog>>());
var JournalPages = await Queryable<JournalPage>().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<JournalPage>(JournalPages).UpdateColumns(c => c.JournalCatalogId).ExecuteCommandAsync();
return true;
});
return result;
}
public async Task<long> InsertAsync(JournalCatalogInput input)
{
var map = input.Adapt<JournalCatalog>();
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<Journal>().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<bool> UpdateAsync(JournalCatalogUpdateInput input)
{
var result = await base.Updateable(input.Adapt<JournalCatalog>()).Where(w => w.Id == input.Id).ExecuteCommandAsync() > 0;
return result;
}
public async Task<bool> 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<bool> 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<bool> 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<List<IcrJournalCatalog>> DeepCopyAsync(IcrJournalCatalog source, long targetParentId, List<IcrJournalCatalog>? list = null)
//{
// list ??= new List<IcrJournalCatalog>();
// 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;
//}
}

View File

@ -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<Journal> JournalRepository,
OssService ossService,
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
IRabbitMQService rabbitMqService,
BaseRepository<JournalPageTask> JournalPageTaskRepository) : BaseRepository<JournalPage>, IJournalPageService
{
public async Task<long> 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<JournalPage>();
//var dotMatrixpages = new List<DotMatrixPage>();
//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<bool> 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;
}
/// <summary>
/// 修改书页的点阵码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
public async Task<bool> 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;
}
/// <summary>
/// 打印书页(全部)
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
public async Task<bool> 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<JournalPageV2Output> DetailAsync(long id)
{
var output = await Queryable()
//.LeftJoin<DotMatrixPage>((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<IcrJournalAssignTask>()
.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<IcrJournalAssignTask>().Where(m => a.Id == m.JournalPageTaskId).Any()
})
.ToListAsync();
}
//if (output?.Areas != null)
// output.Areas = await _JournalPageOtherRepository.Queryable().Where(w => w.JournalPageId == output.JournalPageId).Select<JournalPageOtherOutput>().ToListAsync();
return output;
}
public async Task<bool> DeleteAsync(long id)
{
return await Deleteable().Where(d => d.Id == id).ExecuteCommandAsync() > 0;
}
//public async Task<List<JournalPageNoArticleOutput>> 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<JournalDotPage_Answer> AnswerList { get; set; }
public long TaskId { get; set; }
}

View File

@ -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<Journal> journalRepository, OssService ossService) : BaseRepository<JournalPageTask>, IJournalPageTaskService
{
public async Task<long?> 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<JournalPageTask>();
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<bool> 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<bool> KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input)
{
return await base.Updateable().SetColumns(s => s.KeywordAnalysis, input.KeywordAnalysis).Where(w => w.GroupId == input.Id).ExecuteCommandAsync() > 0;
}
public async Task<JournalPageTaskOutput> DetailAsync(long id)
{
var data = await base.Queryable()
.Where(w => w.Id == id)
.Select(w => new JournalPageTaskOutput()
, true).FirstAsync();
return data;
}
public async Task<bool> 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<bool> 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<JournalPageTask>()).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;
}
}

View File

@ -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<JournalPage> JournalPageRepository,
BaseRepository<JournalPageTask> JournalPageTaskRepository,
BaseRepository<DotFile> dotFileRepository,
BaseRepository<DotFileDetail> dotFileDetailRepository, OssService ossService, ILogger<JournalService> logger) : BaseRepository<Journal>, IJournalService
{
/// <summary>
/// 查询List
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<List<JournalDto>> 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;
}
/// <summary>
/// 分页查询
/// </summary>
/// <param name="search"></param>
/// <returns></returns>
public async Task<PageListModel<JournalDto>> GetPageListAsync(PageQueryModel<JournalQueryDto> search)
{
RefAsync<int> totalNumber = 0;
var dataList = await Queryable()
//.InnerJoin<IcrJournalOrganization>((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<JournalDto>(dataList, search.PageIndex, search.PageSize, totalNumber);
}
/// <summary>
/// 编辑
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseResponse<bool>> 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<bool>.Success(res);
}
public async Task<List<JournalTaskOutput>> 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<JournalDto> DetailAsync(long id)
{
var Journal = await base.Queryable().Where(w => w.Id == id).Select<JournalDto>().FirstAsync();
return Journal;
}
public async Task<bool> DeleteAsync(List<long> 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;
}
/// <summary>
///
/// </summary>
/// <param name="id">书id</param>
/// <param name="index">起始页码</param>
/// <returns></returns>
public async Task<bool> 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<DotMatrixOutput> 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<bool> 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<bool> StatusAsync(long id, JournalStatusEnum status)
{
var book = await base.GetByIdAsync(id);
var tasks = await Context.Queryable<JournalPageTask>().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;
}
}

View File

@ -253,79 +253,103 @@ public class PetService(
throw new BusinessException("宠物未激活,无法喂养", 400); 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;
}
/// <summary>
/// 喂养宠物(无事务,需在外部事务中调用)
/// </summary>
public async Task<FeedPetOutput> 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 growthBefore = pet.GrowthPoints;
var growthAfter = growthBefore + input.GrowthPoints; var growthAfter = growthBefore + input.GrowthPoints;
var hasEvolved = false; var hasEvolved = false;
string? evolvedStageName = null; string? evolvedStageName = null;
// 事务保证一致性 // 累加成长值和喂养次数
await UseTranAsync(async () => var updateResult = await petRepository.Context.Updateable<UserPet>()
.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)
{ {
// 累加成长值和喂养次数 throw new BusinessException("更新宠物成长值失败", 500);
var updateResult = await petRepository.Context.Updateable<UserPet>() }
.SetColumns(p => p.GrowthPoints == growthAfter)
.SetColumns(p => p.FeedingCount == p.FeedingCount + 1) // 进化检查查找下一阶段进化形态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<UserPet>()
.SetColumns(p => p.CurrentEvolutionId == nextEvolution.Id)
.SetColumns(p => p.UpdatedAt == DateTime.Now) .SetColumns(p => p.UpdatedAt == DateTime.Now)
.SetColumns(p => p.UpdatedBy == userId.ToString()) .SetColumns(p => p.UpdatedBy == userId.ToString())
.Where(p => p.Id == input.PetId) .Where(p => p.Id == input.PetId)
.ExecuteCommandAsync(); .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() var record = new PetFeedingRecord
.Where(e => e.PreviousEvolutionId == pet.CurrentEvolutionId {
&& e.RequiredGrowth <= growthAfter PetId = input.PetId,
&& e.Status == (int)DefaultStatusEnum.Active) UserId = userId,
.OrderBy(e => e.RequiredGrowth, OrderByType.Desc) PointsUsed = 0, // 预留:后期可扩展为消耗积分喂养
.FirstAsync(); 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 insertResult = await feedingRecordRepository.InsertAsync(record);
{ if (!insertResult)
// 触发进化 {
var evolveResult = await petRepository.Context.Updateable<UserPet>() throw new BusinessException("写入喂养记录失败", 500);
.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);
}
});
logger.LogInformation("喂养宠物成功PetId: {PetId}, 成长值: {Before} -> {After}, 进化: {HasEvolved}", logger.LogInformation("喂养宠物成功PetId: {PetId}, 成长值: {Before} -> {After}, 进化: {HasEvolved}",
input.PetId, growthBefore, growthAfter, hasEvolved); input.PetId, growthBefore, growthAfter, hasEvolved);

View File

@ -158,7 +158,7 @@ public class UsersService(
if (journalDict.TryGetValue(item.JournalId, out var journal)) if (journalDict.TryGetValue(item.JournalId, out var journal))
{ {
item.JournalTitle = journal.Title; item.JournalTitle = journal.Title;
item.CoverImageUrl = journal.CoverImageUrl; item.CoverImageUrl = journal.Cover;
} }
} }
} }

View File

@ -1,8 +1,12 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; 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.Dto;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
using System.Security.Claims; using System.Security.Claims;
using System.Web;
namespace QYZH.InteractiveMagazine.WebApi.Controllers; namespace QYZH.InteractiveMagazine.WebApi.Controllers;
@ -75,4 +79,57 @@ public abstract class BaseController : ControllerBase
{ {
return Ok(BaseResponse.Fail(msg)); return Ok(BaseResponse.Fail(msg));
} }
/// <summary>
/// 导出Excel
/// </summary>
/// <param name="path">完整文件路径</param>
/// <param name="fileName">带扩展文件名</param>
/// <returns></returns>
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<T>(List<T> list, string fileName)
{
IWebHostEnvironment webHostEnvironment = ServiceContext.GetService<IWebHostEnvironment>();
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<T>(List<T> list, string sheetName, string fileName)
{
IWebHostEnvironment webHostEnvironment = ServiceContext.GetService<IWebHostEnvironment>();
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);
}
} }

View File

@ -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
{
/// <summary>
/// 期刊管理
/// </summary>
[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
/// <summary>
/// 根据ID查询书籍数据
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet, Route("Journal/detail/{id:long}")]
public async Task<BaseResponse<JournalDto>> GetByIdAsync([Required] long id)
{
var result = await JournalService.GetByExpressionAsync<JournalDto>(c => c.Id == id);
return BaseResponse<JournalDto>.Success(result);
}
/// <summary>
/// 分页查询书籍列表
/// </summary>
/// <param name="search">查询条件</param>
/// <returns></returns>
[HttpPost, Route("Journal/pagelist")]
public async Task<BaseResponse<PageListModel<JournalDto>>> GetPageListAsync([FromBody] PageQueryModel<JournalQueryDto> search)
{
var result = await JournalService.GetPageListAsync(search);
return BaseResponse<PageListModel<JournalDto>>.Success(result);
}
/// <summary>
/// 查询书籍列表
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost, Route("Journal/list")]
public async Task<BaseResponse<List<JournalDto>>> GetListAsync([FromQuery] JournalQueryDto dto)
{
var result = await JournalService.GetListAsync(dto);
return BaseResponse<List<JournalDto>>.Success(result);
}
///// <summary>
///// 创建书籍
///// </summary>
///// <param name="dto">书籍对象</param>
///// <returns></returns>
//[HttpPost, Route("Journal/add")]
//public async Task<BaseResponse<long>> AddAsync([FromBody] JournalEditDto dto)
//{
// var result = await JournalService.AddAsync(dto);
// return result;
//}
/// <summary>
/// 编辑书籍
/// </summary>
/// <param name="dto">书籍对象</param>
/// <returns></returns>
[HttpPost, Route("Journal/update")]
public async Task<BaseResponse> Edit([FromBody] JournalEditDto dto)
{
var result = await JournalService.EditAsync(dto);
return result;
}
/// <summary>
/// 批量删除
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[HttpPost, Route("Journal/delete")]
public async Task<BaseResponse<bool>> DeleteAsync([Required][FromBody] List<long> ids)
{
var result = await JournalService.DeleteAsync(ids);
return BaseResponse<bool>.Success(result);
}
/// <summary>
/// 书籍起始页
/// </summary>
/// <returns></returns>
[HttpPost, Route("Journal/startpage")]
public async Task<BaseResponse<bool>> StartPageAsync([FromQuery]long id, [FromQuery] int index)
{
var data = await JournalService.StartPageAsync(id, index);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书籍归档
/// </summary>
/// <returns></returns>
[HttpPost, Route("Journal/rchive/{id:long}")]
public async Task<BaseResponse<bool>> Archive(long id)
{
var data = await JournalService.StatusAsync(id, JournalStatusEnum.Archive);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书籍废弃
/// </summary>
/// <returns></returns>
[HttpPost]
[HttpPost, Route("Journal/abandon/{id:long}")]
public async Task<BaseResponse<bool>> Abandon(long id)
{
var data = await JournalService.StatusAsync(id, JournalStatusEnum.Abandoned);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书籍铺码
/// </summary>
/// <returns></returns>
[HttpPost, Route("Journal/printcode/{id:long}")]
public async Task<BaseResponse<DotMatrixOutput>> PrintCodeAsync(long id)
{
var data = await JournalService.PrintCodeAsync(id);
return BaseResponse<DotMatrixOutput>.Success(data);
}
/// <summary>
/// 上报铺码结果
/// </summary>
/// <param name="input"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpPost, Route("Journal/resultreport")]
public async Task<BaseResponse<bool>> 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<bool>.Success(data);
}
/// <summary>
/// 书籍导入
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost, Route("Journal/import")]
public async Task<BaseResponse<long>> JournalImport(JournalImportDto input)
{
var data = await JournalCatalogService.ImportAsync(input);
return BaseResponse<long>.Success(data);
}
///// <summary>
///// 工单分页列表
///// </summary>
///// <param name="search"></param>
///// <returns></returns>
//[HttpPost, Route("tasklibrary/workorder/pagelist")]
//public async Task<BaseResponse<PageListModel<IcrJournalWorkOrderDto>>> WorkOrderPageListAsync([FromBody] PageQueryModel<IcrWorkOrderQueryDto> search)
//{
// var data = await JournalWorkOrderService.WorkOrderPageListAsync(search);
// return BaseResponse<PageListModel<IcrJournalWorkOrderDto>>.Success(data);
//}
/// <summary>
/// 书籍发起工单
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
//[HttpPost, Route("tasklibrary/workorder/create")]
//public async Task<BaseResponse<bool>> CreateWorkOrderAsync([FromBody] IcrWorkOrderCreateDto dto)
//{
// var result = await JournalWorkOrderService.CreateWorkOrderAsync(dto);
// return result;
//}
///// <summary>
///// 更新工单状态
///// </summary>
///// <param name="JournalId"></param>
///// <returns></returns>
//[HttpPost, Route("tasklibrary/workorder/setstatus/{JournalId:long}")]
//public async Task<BaseResponse<int>> UpdateWorkOrderAsync(long JournalId)
//{
// var result = await JournalWorkOrderService.UpdateWorkOrderAsync(JournalId);
// return BaseResponse<int>.Success(result);
//}
/// <summary>
/// 题库代理请求接口
/// </summary>
/// <returns></returns>
[HttpGet, Route("tasklibrary/proxyget")]
public async Task<IActionResult> 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
///// <summary>
///// 书籍目录详情
///// </summary>
///// <returns></returns>
//[HttpGet, Route("catalog/tree/{JournalId:long}")]
//public async Task<BaseResponse<List<IcrJournalCatalog>>> CatalogDetailAsync(long JournalId)
//{
// var data = await _JournalCatalogService.DetailAsync(JournalId);
// return BaseResponse<List<IcrJournalCatalog>>.Success(data);
//}
/// <summary>
/// 书籍目录详情
/// </summary>
/// <returns></returns>
[HttpGet, Route("catalog/tree/{JournalId:long}")]
public async Task<BaseResponse<List<IcrJournalCatalogTreeDto>>> CatalogDetailAsync(long JournalId)
{
var data = await JournalCatalogService.GetJournalCataloTreeAsync(JournalId);
return BaseResponse<List<IcrJournalCatalogTreeDto>>.Success(data);
}
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
[HttpGet, Route("catalog/list")]
public async Task<BaseResponse<List<JournalCatalogTreeListDto>>> GetJournalCatelogListAsync(long JournalId)
{
var data = await JournalCatalogService.GetJournalCatalogListAsync(JournalId);
return BaseResponse<List<JournalCatalogTreeListDto>>.Success(data);
}
/// <summary>
/// 下载书籍目录导入模板
/// </summary>
/// <returns></returns>
[HttpGet, Route("catalog/template")]
[AllowAnonymous]
public IActionResult ImportCatalogTemplateExcel()
{
var result = DownloadImportTemplate(new List<JournalCatalogTreeListDto>() { }, "书籍目录导入模板");
return ExportExcel(result.Item2, result.Item1);
}
/// <summary>
/// 书籍目录导出
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
[HttpGet, Route("catalog/export/{JournalId}")]
public async Task<IActionResult> CatalogExport(long JournalId)
{
var list = await JournalCatalogService.GetJournalCatalogListAsync(JournalId);
var result = ExportExcelMini(list, "sheet1", "书籍目录导出");
return ExportExcel(result.Item2, result.Item1);
}
/// <summary>
/// 书籍目录导入
/// </summary>
/// <param name="JournalId">file</param>
/// <param name="file">file</param>
/// <returns></returns>
[HttpPost, Route("catalog/import/{JournalId}")]
public async Task<BaseResponse<bool>> ImportAsync(long JournalId, [FromForm(Name = "file")] IFormFile file)
{
List<JournalCatalogTreeListDto> dto = new();
using (var stream = file.OpenReadStream())
{
dto = stream.Query<JournalCatalogTreeListDto>().ToList();
}
var result = await JournalCatalogService.ImportCatalogAsync(JournalId, dto);
return BaseResponse<bool>.Success(result);
}
/// <summary>
/// 书籍目录新增
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost, Route("catalog/add")]
public async Task<BaseResponse<long>> CatalogAdd(JournalCatalogInput input)
{
var data = await JournalCatalogService.InsertAsync(input);
return BaseResponse<long>.Success(data);
}
/// <summary>
/// 书籍目录修改
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost, Route("catalog/update")]
public async Task<BaseResponse<bool>> CatalogUpdate(JournalCatalogUpdateInput input)
{
var data = await JournalCatalogService.UpdateAsync(input);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书籍目录删除
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpPost, Route("catalog/delete/{id:long}")]
public async Task<BaseResponse<bool>> CatalogDelete(long id)
{
var data = await JournalCatalogService.DeleteAsync(id);
return BaseResponse<bool>.Success(data);
}
///// <summary>
///// 书籍目录复制
///// </summary>
///// <param name="input"></param>
///// <returns></returns>
//[HttpPost, Route("catalog/copy")]
//[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
//public async Task<BaseResponse> CatalogCopy(CopyInput input)
//{
// var data = await _JournalCatalogServices.CopyAsync(input);
// return ApiResult(data, "书籍目录删除失败!");
//}
/// <summary>
/// 书籍目录移动
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost, Route("catalog/move")]
public async Task<BaseResponse<bool>> CatalogMove(MoveInput input)
{
var data = await JournalCatalogService.MoveAsync(input);
return BaseResponse<bool>.Success(data);
}
#endregion
#region
/// <summary>
/// 书页新增
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/add")]
public async Task<BaseResponse<long>> PageAdd(JournalAddV2Input input)
{
var data = await JournalPageService.InsertAsync(input);
return BaseResponse<long>.Success(data);
}
/// <summary>
/// 书页修改
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/update")]
public async Task<BaseResponse<bool>> PageUpdate(PageLayoutInput input)
{
var data = await JournalPageService.UpdateAsync(input);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 自动铺码
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/updatepageno/{JournalId:long}")]
public async Task<BaseResponse<bool>> PageUpdatePageNo([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId)
{
var data = await JournalPageService.UpdatePageNoAsync(JournalId);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 下载书籍页码点阵码PDF文件名称
/// </summary>
/// <returns></returns>
[HttpGet, Route("page/downloadJournalpagePdf/{JournalId:long}")]
public async Task<IActionResult> 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)));
}
///// <summary>
///// 打印书页(全部) 暂时废弃这个方,直接使用 Adobe Reader X 软件打印就可以了
///// </summary>
///// <returns></returns>
//[HttpPost, Route("page/printJournalPage/{JournalId:long}")]
//public async Task<BaseResponse<bool>> PrintJournalPage([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId)
//{
// var data = await JournalPageService.PrintJournalPageAsync(JournalId);
// return BaseResponse<bool>.Success(data);
//}
///// <summary>
///// 书页删除
///// </summary>
///// <returns></returns>
//[HttpPost, Route("page/delete/{id:long}")]
//[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
//public async Task<IActionResult> PageDelete(long id)
//{
// var data = await _JournalPageServices.DeleteAsync(id);
// return ApiResult(data, "更新书页失败!");
//}
/// <summary>
/// 书页详情
/// </summary>
/// <returns></returns>
[HttpGet, Route("page/detail/{id:long}")]
public async Task<BaseResponse<JournalPageV2Output>> PageDetail(long id)
{
var data = await JournalPageService.DetailAsync(id);
return BaseResponse<JournalPageV2Output>.Success(data);
}
///// <summary>
///// 不包含有文章的书页
///// </summary>
///// <returns></returns>
//[HttpGet, Route("page/no-article/{JournalId:long}")]
//[ProducesResponseType(typeof(BaseResponse<List<JournalPageNoArticleOutput>>), 200)]
//public async Task<IActionResult> PageNoArticleAsync(long JournalId)
//{
// var data = await _JournalPageService.PageNoArticleAsync(JournalId);
// return BaseResponse<List<JournalPageNoArticleOutput>>(data, "查询书页失败!");
//}
#endregion
#region
/// <summary>
/// 书页问题新增
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/task/add")]
[ProducesResponseType(typeof(BaseResponse<long>), 200)]
public async Task<BaseResponse<long>> PageTaskAdd(JournalPageTaskAddInput input)
{
var data = await JournalPageTaskService.InsertAsync(input);
return BaseResponse<long>.Success(data.Value);
}
/// <summary>
/// 书页问题单项解析
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/task/keyword-analysis")]
[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
public async Task<BaseResponse<bool>> KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input)
{
var data = await JournalPageTaskService.KeywordAnalysisAsync(input);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书页问题更新
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/task/update")]
[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
public async Task<BaseResponse<bool>> PageTaskUpdate(JournalPageTaskUpdateInput input)
{
var data = await JournalPageTaskService.UpdateAsync(input);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书页问题详情 + 题库那边需要用
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[HttpGet, Route("page/task/detail/{id:long}")]
public async Task<BaseResponse<JournalPageTaskOutput>> PageTaskDetail(long id)
{
var data = await JournalPageTaskService.DetailAsync(id);
return BaseResponse<JournalPageTaskOutput>.Success(data);
}
/// <summary>
/// 书页问题删除
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/task/delete/{id:long}")]
public async Task<BaseResponse<bool>> PageTaskDelete(long id)
{
var data = await JournalPageTaskService.DeleteAsync(id);
return BaseResponse<bool>.Success(data);
}
/// <summary>
/// 书页问题补充
/// </summary>
/// <returns></returns>
[HttpPost, Route("page/task/complement")]
public async Task<BaseResponse<bool>> ComplementAnsync(JournalPageTaskComplementInput input)
{
var data = await JournalPageTaskService.ComplementAsync(input);
return BaseResponse<bool>.Success(data);
}
#endregion
}
}

View File

@ -25,6 +25,7 @@ using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI; using Swashbuckle.AspNetCore.SwaggerUI;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Yitter.IdGenerator; using Yitter.IdGenerator;
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -51,6 +52,9 @@ builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection")); .PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection"));
//redis //redis
builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings")); builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
//MQ
builder.Services.AddRabbitMQ(builder.Configuration);
// 配置Serilog // 配置Serilog
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration) .ReadFrom.Configuration(builder.Configuration)

View File

@ -13,12 +13,13 @@
"Sentinels": [], "Sentinels": [],
"ExpireSecondRange": [ 3600, 7200 ] "ExpireSecondRange": [ 3600, 7200 ]
}, },
"RabbitMQSettings": { "RabbitMq": {
"HostName": "192.168.20.150", "HostName": "192.168.20.150",
"Port": 5672, "Port": 5672,
"UserName": "smartschool", "UserName": "smartschool",
"Password": "@ss%&*otz%d*pq2S", "Password": "@ss%&*otz%d*pq2S",
"VirtualHost": "InteractiveMagazine" "VirtualHost": "InteractiveMagazine",
"ClientProvidedName": "Custom connection name"
}, },
"WeChatSettings": { "WeChatSettings": {
"AppId": "wx7922cc9b6023f3ac", "AppId": "wx7922cc9b6023f3ac",