diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs
index e910085..a6dda42 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskAddInput.cs
@@ -29,5 +29,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
///
public TaskBankTypeEnum Type { get; set; }
+ ///
+ /// 是否需要AI批改,默认需要
+ ///
+ public bool NeedAiProcess { get; set; } = true;
+
}
}
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
index 7bb9e41..44f11b6 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
@@ -74,6 +74,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// Prompt配置
///
public string Prompt { get; set; }
+
+ ///
+ /// 是否需要AI批改
+ ///
+ public bool NeedAiProcess { get; set; }
}
///
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs
index c797da2..9cbaf60 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs
@@ -71,6 +71,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
///
public string Prompt { get; set; }
+ ///
+ /// 是否需要AI批改,false时由人工批改
+ ///
+ public bool NeedAiProcess { get; set; } = true;
+
}
public class JournalPageTaskComplementInput
diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs
index dfb1dfd..43f18e7 100644
--- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs
+++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs
@@ -2,9 +2,9 @@ using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
- ///
- ///杂志任务用户作答记录;
- ///
+ ///
+ /// 杂志任务用户作答记录
+ ///
[SugarTable("JournalPageTaskUserAnswer")]
public class JournalPageTaskUserAnswer : SqlSugarBaseEntity
{
@@ -12,204 +12,231 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Desc:书id
/// Default:
/// Nullable:False
- ///
+ ///
public long JournalId { get; set; }
///
/// Desc:书页id
/// Default:
/// Nullable:False
- ///
+ ///
public long JournalPageId { get; set; }
///
/// Desc:任务id
/// Default:
/// Nullable:False
- ///
+ ///
public long JournalPageTaskId { get; set; }
///
/// Desc:任务分组id
/// Default:
/// Nullable:False
- ///
+ ///
public long JournalPageTaskGroupId { get; set; }
///
/// Desc:用户id
/// Default:
/// Nullable:False
- ///
+ ///
public long UserId { get; set; }
///
/// Desc:作答结果
/// Default:
/// Nullable:True
- ///
+ ///
public string Result { get; set; }
///
/// Desc:积分
/// Default:0
/// Nullable:False
- ///
- public float Points { get; set; }
+ ///
+ public int Points { get; set; }
///
/// Desc:成长值
/// Default:0
/// Nullable:False
- ///
+ ///
public float GrowthPoints { get; set; }
///
/// Desc:题目得分
/// Default:0
/// Nullable:False
- ///
+ ///
public float Score { get; set; }
+ ///
+ /// Desc:理解力评分
+ /// Default:0
+ /// Nullable:False
+ ///
+ public float Comprehension { get; set; }
+
+ ///
+ /// Desc:判断力评分
+ /// Default:0
+ /// Nullable:False
+ ///
+ public float Judgment { get; set; }
+
+ ///
+ /// Desc:表达力评分
+ /// Default:0
+ /// Nullable:False
+ ///
+ public float Expression { get; set; }
+
+ ///
+ /// Desc:说服力评分
+ /// Default:0
+ /// Nullable:False
+ ///
+ public float Persuasiveness { get; set; }
+
///
/// Desc:题目作答图片地址
/// Default:
/// Nullable:True
- ///
+ ///
public string QuestionAnswerUrl { get; set; }
///
/// Desc:答案图片地址
/// Default:
/// Nullable:True
- ///
+ ///
public string AnswerUrl { get; set; }
///
/// Desc:页面作答图片地址
/// Default:
/// Nullable:True
- ///
+ ///
public string PageAnswerUrl { get; set; }
///
/// Desc:乐观锁
/// Default:
/// Nullable:False
- ///
+ ///
public int Revision { get; set; }
///
/// Desc:作答状态
/// Default:
/// Nullable:False
- ///
+ ///
public int AnswerStatus { get; set; }
///
/// Desc:作答开始时间
/// Default:
/// Nullable:True
- ///
+ ///
public DateTime AnswerStartTime { get; set; }
///
/// Desc:作答结束时间
/// Default:
/// Nullable:True
- ///
+ ///
public DateTime AnswerEndTime { get; set; }
///
/// Desc:作答耗时(秒)
/// Default:0
/// Nullable:False
- ///
+ ///
public int AnswerSeconds { get; set; }
///
/// Desc:图像识别结果
/// Default:
/// Nullable:False
- ///
+ ///
public int ImageRecognition { get; set; }
///
/// Desc:书页序号
/// Default:
/// Nullable:False
- ///
+ ///
public int JournalPageNum { get; set; }
///
/// Desc:修改次数
/// Default:
/// Nullable:False
- ///
+ ///
public int Modify { get; set; }
///
/// Desc:最后标签id
/// Default:
/// Nullable:False
- ///
+ ///
public long LastTag { get; set; }
///
/// Desc:点读书页序号
/// Default:
/// Nullable:False
- ///
+ ///
public int DotPageNum { get; set; }
///
/// Desc:页面结果图片地址
/// Default:
/// Nullable:True
- ///
+ ///
public string PageResultUrl { get; set; }
///
/// Desc:题型
/// Default:
/// Nullable:True
- ///
+ ///
public string Type { get; set; }
///
/// Desc:点读书页编号
/// Default:
/// Nullable:True
- ///
+ ///
public string DotPageNo { get; set; }
///
/// Desc:页面作答点读图片地址
/// Default:
/// Nullable:True
- ///
+ ///
public string PageAnswerDotUrl { get; set; }
///
/// Desc:中断次数
/// Default:0
/// Nullable:False
- ///
+ ///
public int BreakCount { get; set; }
///
/// Desc:中断时间记录
/// Default:
/// Nullable:True
- ///
+ ///
public string BreakTimes { get; set; }
///
/// Desc:作业状态
/// Default:
/// Nullable:True
- ///
+ ///
public string AssignmentStatus { get; set; }
-
}
}
diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswerSnapshot.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswerSnapshot.cs
index 300176e..6bf3ef3 100644
--- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswerSnapshot.cs
+++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswerSnapshot.cs
@@ -46,7 +46,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///
/// Points awarded.
///
- public float Points { get; set; }
+ public int Points { get; set; }
///
/// Growth points awarded.
@@ -58,6 +58,26 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///
public float Score { get; set; }
+ ///
+ /// Comprehension score.
+ ///
+ public float Comprehension { get; set; }
+
+ ///
+ /// Judgment score.
+ ///
+ public float Judgment { get; set; }
+
+ ///
+ /// Expression score.
+ ///
+ public float Expression { get; set; }
+
+ ///
+ /// Persuasiveness score.
+ ///
+ public float Persuasiveness { get; set; }
+
///
/// Question answer image URL.
///
diff --git a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
index efb5ae4..4976a0a 100644
--- a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
+++ b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
@@ -24,6 +24,7 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
var map = input.Adapt();
map.Id = YitIdHelper.NextId();
map.GroupId = map.Id;
+ map.NeedAiProcess = input.NeedAiProcess;
var no = input.No.Split('-').Select(int.Parse).ToArray();
if (no.Last() >= 2)
{
@@ -59,7 +60,6 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
task.Type = input.Type;
task.Task = input.Task;
- task.No = input.No;
task.Points = input.Points;
task.GrowthPoint = (int?)input.GrowthPoint;
task.Comprehension = input.Comprehension;
@@ -68,6 +68,7 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
task.Persuasiveness = input.Persuasiveness;
task.AnswerTime = input.AnswerTime;
task.Prompt = input.Prompt;
+ task.NeedAiProcess = input.NeedAiProcess;
var no = input.No.Split('-').Select(int.Parse).ToArray();
@@ -104,7 +105,8 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
Expression = task.Expression,
Persuasiveness = task.Persuasiveness,
AnswerTime = task.AnswerTime,
- Prompt = task.Prompt
+ Prompt = task.Prompt,
+ NeedAiProcess = task.NeedAiProcess
};
// 查询答案列表
diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs
index a44f653..d3267b0 100644
--- a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs
+++ b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs
@@ -1,21 +1,27 @@
-using Newtonsoft.Json;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
+using QYZH.InteractiveMagazine.Models.Entity;
+using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
+using System.Net.Http.Headers;
using System.Text;
-using System.Threading.Channels;
-using Yitter.IdGenerator;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.WorkService.Consumers;
///
-/// 期刊任务接收消费者(示例)
+/// 期刊任务接收消费者
///
-public class JournalTaskReceiveConsumer(ILogger logger, IConfiguration configuration,
+public class JournalTaskReceiveConsumer(
+ ILogger logger,
+ IConfiguration configuration,
IServiceScopeFactory scopeFactory,
- IWebHostEnvironment webHostEnvironment,
IHttpClientFactory httpClientFactory,
OssService ossService) : IQueueConsumer
{
+ private const int DefaultAiScoreMaxRetryCount = 3;
+ private const int DefaultAiScoreRetryDelayMilliseconds = 1000;
+ private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
public string Exchange => "ex.journal";
@@ -30,21 +36,98 @@ public class JournalTaskReceiveConsumer(ILogger logg
using var scope = scopeFactory.CreateScope();
var client = scope.ServiceProvider.GetRequiredService();
- // TODO: 在此编写具体的消息处理逻辑
+ var data = JsonSerializer.Deserialize(message, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ }) ?? throw new InvalidOperationException("期刊任务消息内容为空");
+
+ if (data.Questions == null || data.Questions.Length == 0)
+ {
+ logger.LogWarning("期刊任务消息没有题目,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}", data.UserId, data.JournalId, data.PageId);
+ return;
+ }
+
+ var taskIds = data.Questions.Select(q => q.Id).Distinct().ToList();
+ var tasks = await client.Queryable()
+ .Where(t => taskIds.Contains(t.Id) && !t.IsDeleted)
+ .ToListAsync(cancellationToken);
+ var taskMap = tasks.ToDictionary(t => t.Id);
+ var referenceAnswers = await client.Queryable()
+ .Where(a => taskIds.Contains(a.JournalPageTaskId) && !a.IsDeleted)
+ .ToListAsync(cancellationToken);
+ var referenceAnswerMap = referenceAnswers
+ .GroupBy(a => a.JournalPageTaskId)
+ .ToDictionary(g => g.Key, g => g.ToList());
+
+ var page = await client.Queryable()
+ .Where(p => p.Id == data.PageId && !p.IsDeleted)
+ .FirstAsync(cancellationToken);
+
+ var answerEntities = new List();
+ foreach (var question in data.Questions)
+ {
+ if (!taskMap.TryGetValue(question.Id, out var task))
+ {
+ logger.LogWarning("未找到期刊任务,TaskId: {TaskId}, UserId: {UserId}", question.Id, data.UserId);
+ continue;
+ }
+
+ if (!task.NeedAiProcess)
+ {
+ logger.LogInformation("期刊任务配置为人工批改,跳过AI评分,TaskId: {TaskId}, UserId: {UserId}", task.Id, data.UserId);
+ continue;
+ }
+
+ referenceAnswerMap.TryGetValue(task.Id, out var taskReferenceAnswers);
+ var scoreResult = await ScoreQuestionAsync(task, question, taskReferenceAnswers ?? [], cancellationToken);
+ answerEntities.Add(BuildAnswerEntity(data, question, task, page, scoreResult));
+ }
+
+ if (answerEntities.Count == 0)
+ {
+ logger.LogWarning("期刊任务消息没有可入库的答题记录,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}", data.UserId, data.JournalId, data.PageId);
+ return;
+ }
+
+ client.Ado.BeginTran();
try
{
- var data = System.Text.Json.JsonSerializer.Deserialize(message);
+ foreach (var answer in answerEntities)
+ {
+ var existing = await client.Queryable()
+ .Where(a => a.UserId == answer.UserId && a.JournalPageTaskId == answer.JournalPageTaskId && !a.IsDeleted)
+ .FirstAsync(cancellationToken);
+ if (existing == null)
+ {
+ await client.Insertable(answer).ExecuteCommandAsync(cancellationToken);
+ continue;
+ }
+ await client.Insertable(BuildAnswerSnapshot(existing)).ExecuteCommandAsync(cancellationToken);
+
+ answer.Id = existing.Id;
+ answer.CreatedBy = existing.CreatedBy;
+ answer.CreatedAt = existing.CreatedAt;
+ answer.UpdatedBy = answer.UserId.ToString();
+ answer.UpdatedAt = DateTime.Now;
+
+ await client.Updateable(answer)
+ .IgnoreColumns(a => new { a.CreatedBy, a.CreatedAt })
+ .Where(a => a.Id == existing.Id)
+ .ExecuteCommandAsync(cancellationToken);
+ }
client.Ado.CommitTran();
}
- catch (Exception ex)
+ catch
{
client.Ado.RollbackTran();
- logger.LogError(ex.Message + ex.StackTrace);
+ throw;
}
- await Task.CompletedTask;
+
+ logger.LogInformation("期刊任务答题记录保存完成,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}, Count: {Count}",
+ data.UserId, data.JournalId, data.PageId, answerEntities.Count);
}
public Task OnErrorAsync(byte[] body, Exception exception)
@@ -54,32 +137,684 @@ public class JournalTaskReceiveConsumer(ILogger logg
return Task.CompletedTask;
}
+ private async Task ScoreQuestionAsync(
+ JournalPageTask task,
+ Question question,
+ List referenceAnswers,
+ CancellationToken cancellationToken)
+ {
+ var apiKey = configuration["AiChat:ApiKey"];
+ var baseUrl = configuration["AiChat:BaseUrl"];
+ var model = configuration["AiChat:Model"];
+ var timeoutSeconds = configuration.GetValue("AiChat:TimeoutSeconds");
+ var maxTokens = configuration.GetValue("AiChat:MaxTokens");
+ var temperature = configuration.GetValue("AiChat:Temperature");
+ if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
+ {
+ throw new InvalidOperationException("AI聊天服务配置不完整,请检查 AiChat 配置节点");
+ }
+
+ var answerImages = await BuildAnswerImageContentsAsync(question, cancellationToken);
+ if (answerImages.Count == 0)
+ {
+ throw new InvalidOperationException($"题目 {question.Id} 缺少答案图片");
+ }
+
+ var referenceAnswerImages = await BuildReferenceAnswerImageContentsAsync(task.Id, referenceAnswers, cancellationToken);
+
+ var content = new List