2026-06-30 16:16:12 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Entity;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Models.Enum;
|
2026-06-29 16:34:26 +08:00
|
|
|
|
using SqlSugar;
|
2026-06-11 18:01:07 +08:00
|
|
|
|
using System.Text;
|
2026-06-30 16:16:12 +08:00
|
|
|
|
using System.Text.Json;
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
|
|
|
|
|
namespace QYZH.InteractiveMagazine.WorkService.Consumers;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// 期刊任务接收消费者
|
2026-06-11 18:01:07 +08:00
|
|
|
|
/// </summary>
|
2026-06-30 16:16:12 +08:00
|
|
|
|
public class JournalTaskReceiveConsumer(
|
|
|
|
|
|
ILogger<JournalTaskReceiveConsumer> logger,
|
|
|
|
|
|
IConfiguration configuration,
|
2026-07-08 18:00:44 +08:00
|
|
|
|
IServiceScopeFactory scopeFactory) : IQueueConsumer
|
2026-06-11 18:01:07 +08:00
|
|
|
|
{
|
2026-07-01 14:50:37 +08:00
|
|
|
|
private const float DefaultCompletionThreshold = 80;
|
2026-07-02 16:05:13 +08:00
|
|
|
|
private const string AiProcessingMessage = "AI批阅中,请稍后";
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
2026-06-22 16:56:05 +08:00
|
|
|
|
public string Exchange => "ex.journal";
|
|
|
|
|
|
|
|
|
|
|
|
public string QueueName => "mq.journal.task.receive";
|
|
|
|
|
|
|
|
|
|
|
|
public string RoutingKey => "rk.journal.task.receive";
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public async Task HandleAsync(byte[] body, CancellationToken cancellationToken = default)
|
2026-06-11 18:01:07 +08:00
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
var message = Encoding.UTF8.GetString(body);
|
|
|
|
|
|
logger.LogInformation("收到期刊任务消息: {Message}", message);
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
2026-06-29 16:34:26 +08:00
|
|
|
|
using var scope = scopeFactory.CreateScope();
|
|
|
|
|
|
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
2026-06-30 16:16:12 +08:00
|
|
|
|
var data = JsonSerializer.Deserialize<QuestionData>(message, new JsonSerializerOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
PropertyNameCaseInsensitive = true
|
|
|
|
|
|
}) ?? throw new InvalidOperationException("期刊任务消息内容为空");
|
2026-07-06 14:21:21 +08:00
|
|
|
|
data.Normalize();
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
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<JournalPageTask>()
|
|
|
|
|
|
.Where(t => taskIds.Contains(t.Id) && !t.IsDeleted)
|
|
|
|
|
|
.ToListAsync(cancellationToken);
|
|
|
|
|
|
var taskMap = tasks.ToDictionary(t => t.Id);
|
2026-07-02 16:05:13 +08:00
|
|
|
|
var groupIds = tasks.Select(t => t.GroupId > 0 ? t.GroupId : t.Id).Distinct().ToList();
|
|
|
|
|
|
var groupTasks = await client.Queryable<JournalPageTask>()
|
2026-07-08 18:00:44 +08:00
|
|
|
|
.Where(t => (groupIds.Contains(t.GroupId) || groupIds.Contains(t.Id)) && !t.IsDeleted)
|
2026-07-02 16:05:13 +08:00
|
|
|
|
.ToListAsync(cancellationToken);
|
|
|
|
|
|
var persistedTaskMap = groupTasks
|
|
|
|
|
|
.GroupBy(t => t.GroupId > 0 ? t.GroupId : t.Id)
|
|
|
|
|
|
.ToDictionary(
|
|
|
|
|
|
g => g.Key,
|
|
|
|
|
|
g => g.FirstOrDefault(t => t.Id == g.Key)
|
|
|
|
|
|
?? g.OrderBy(t => t.Id).First());
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
2026-07-02 16:05:13 +08:00
|
|
|
|
var pageIds = groupTasks.Select(t => t.JournalPageId).Append(data.PageId).Distinct().ToList();
|
|
|
|
|
|
var pages = await client.Queryable<JournalPage>()
|
|
|
|
|
|
.Where(p => pageIds.Contains(p.Id) && !p.IsDeleted)
|
|
|
|
|
|
.ToListAsync(cancellationToken);
|
|
|
|
|
|
var pageMap = pages.ToDictionary(p => p.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var questionContexts = new List<JournalQuestionContext>();
|
2026-06-30 16:16:12 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 16:05:13 +08:00
|
|
|
|
pageMap.TryGetValue(task.JournalPageId, out var taskPage);
|
|
|
|
|
|
questionContexts.Add(new JournalQuestionContext(question, task, taskPage));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 18:00:44 +08:00
|
|
|
|
var answerContexts = new List<JournalAnswerContext>();
|
2026-07-02 16:05:13 +08:00
|
|
|
|
foreach (var scoreUnit in BuildScoreUnits(questionContexts))
|
|
|
|
|
|
{
|
|
|
|
|
|
var persistedContext = BuildPersistedContext(scoreUnit, persistedTaskMap, pageMap);
|
|
|
|
|
|
if (persistedContext == null)
|
|
|
|
|
|
{
|
2026-07-08 18:00:44 +08:00
|
|
|
|
logger.LogWarning("期刊任务上传单元缺少可入库任务,UserId: {UserId}, GroupId: {GroupId}", data.UserId, scoreUnit.GroupId);
|
2026-07-02 16:05:13 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 18:00:44 +08:00
|
|
|
|
var processingResult = BuildFailureScoreResult();
|
|
|
|
|
|
answerContexts.Add(new JournalAnswerContext(
|
|
|
|
|
|
BuildAnswerEntity(data, scoreUnit, persistedContext.Question, persistedContext.Task, persistedContext.Page, processingResult, GetCompletionThreshold()),
|
|
|
|
|
|
persistedContext.Question,
|
|
|
|
|
|
persistedContext.Task,
|
|
|
|
|
|
processingResult));
|
|
|
|
|
|
LogSkippedGroupTasks(scoreUnit, persistedContext.Task);
|
2026-06-30 16:16:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 14:50:37 +08:00
|
|
|
|
if (answerContexts.Count == 0)
|
2026-06-30 16:16:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
logger.LogWarning("期刊任务消息没有可入库的答题记录,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}", data.UserId, data.JournalId, data.PageId);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client.Ado.BeginTran();
|
2026-06-29 16:34:26 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-07-01 14:50:37 +08:00
|
|
|
|
foreach (var context in answerContexts)
|
2026-06-30 16:16:12 +08:00
|
|
|
|
{
|
2026-07-01 14:50:37 +08:00
|
|
|
|
var answer = context.Answer;
|
2026-06-30 16:16:12 +08:00
|
|
|
|
var existing = await client.Queryable<JournalPageTaskUserAnswer>()
|
|
|
|
|
|
.Where(a => a.UserId == answer.UserId && a.JournalPageTaskId == answer.JournalPageTaskId && !a.IsDeleted)
|
|
|
|
|
|
.FirstAsync(cancellationToken);
|
2026-06-29 16:34:26 +08:00
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
if (existing == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await client.Insertable(answer).ExecuteCommandAsync(cancellationToken);
|
2026-07-08 18:00:44 +08:00
|
|
|
|
continue;
|
2026-07-01 14:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 18:00:44 +08:00
|
|
|
|
answer.Id = existing.Id;
|
|
|
|
|
|
answer.CreatedBy = existing.CreatedBy;
|
|
|
|
|
|
answer.CreatedAt = existing.CreatedAt;
|
|
|
|
|
|
answer.UpdatedBy = answer.UserId.ToString();
|
|
|
|
|
|
answer.UpdatedAt = DateTime.Now;
|
2026-07-01 14:50:37 +08:00
|
|
|
|
|
2026-07-08 18:00:44 +08:00
|
|
|
|
await client.Updateable(answer)
|
|
|
|
|
|
.IgnoreColumns(a => new { a.CreatedBy, a.CreatedAt })
|
|
|
|
|
|
.Where(a => a.Id == existing.Id)
|
|
|
|
|
|
.ExecuteCommandAsync(cancellationToken);
|
2026-06-30 16:16:12 +08:00
|
|
|
|
}
|
2026-06-29 16:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
client.Ado.CommitTran();
|
|
|
|
|
|
}
|
2026-06-30 16:16:12 +08:00
|
|
|
|
catch
|
2026-06-29 16:34:26 +08:00
|
|
|
|
{
|
|
|
|
|
|
client.Ado.RollbackTran();
|
2026-06-30 16:16:12 +08:00
|
|
|
|
throw;
|
2026-06-29 16:34:26 +08:00
|
|
|
|
}
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
2026-07-08 18:00:44 +08:00
|
|
|
|
logger.LogInformation("期刊任务上传数据保存完成,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}, Count: {Count}",
|
2026-07-01 14:50:37 +08:00
|
|
|
|
data.UserId, data.JournalId, data.PageId, answerContexts.Count);
|
2026-06-11 18:01:07 +08:00
|
|
|
|
}
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public Task OnErrorAsync(byte[] body, Exception exception)
|
2026-06-11 18:01:07 +08:00
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
var message = Encoding.UTF8.GetString(body);
|
|
|
|
|
|
logger.LogError(exception, "处理期刊任务消息失败: {Message}", message);
|
2026-06-11 18:01:07 +08:00
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
2026-06-29 16:34:26 +08:00
|
|
|
|
|
2026-07-02 16:05:13 +08:00
|
|
|
|
private static JournalAnswerScoreResult BuildFailureScoreResult()
|
|
|
|
|
|
{
|
|
|
|
|
|
return new JournalAnswerScoreResult
|
|
|
|
|
|
{
|
|
|
|
|
|
Completion = 0,
|
|
|
|
|
|
Result = AiProcessingMessage
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
private static List<JournalScoreUnit> BuildScoreUnits(List<JournalQuestionContext> contexts)
|
|
|
|
|
|
{
|
|
|
|
|
|
return contexts
|
|
|
|
|
|
.GroupBy(c => c.Task.GroupId > 0 ? c.Task.GroupId : c.Task.Id)
|
|
|
|
|
|
.Select(g => new JournalScoreUnit(
|
|
|
|
|
|
g.Key,
|
|
|
|
|
|
g.OrderBy(c => c.Page?.PageNum ?? 0)
|
|
|
|
|
|
.ThenBy(c => ParseTaskNo(c.Task.No))
|
|
|
|
|
|
.ThenBy(c => c.Task.Id)
|
|
|
|
|
|
.ToList()))
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static JournalQuestionContext? BuildPersistedContext(
|
|
|
|
|
|
JournalScoreUnit scoreUnit,
|
|
|
|
|
|
Dictionary<long, JournalPageTask> persistedTaskMap,
|
|
|
|
|
|
Dictionary<long, JournalPage> pageMap)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (scoreUnit.Questions.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var sourceContext = scoreUnit.Questions.FirstOrDefault(q => q.Task.Id == scoreUnit.GroupId)
|
|
|
|
|
|
?? scoreUnit.Questions.First();
|
|
|
|
|
|
var persistedTask = persistedTaskMap.TryGetValue(scoreUnit.GroupId, out var task)
|
|
|
|
|
|
? task
|
|
|
|
|
|
: sourceContext.Task;
|
|
|
|
|
|
pageMap.TryGetValue(persistedTask.JournalPageId, out var persistedPage);
|
|
|
|
|
|
|
|
|
|
|
|
return new JournalQuestionContext(sourceContext.Question, persistedTask, persistedPage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int ParseTaskNo(string? taskNo)
|
|
|
|
|
|
{
|
|
|
|
|
|
return int.TryParse(taskNo, out var no) ? no : int.MaxValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void LogSkippedGroupTasks(JournalScoreUnit scoreUnit, JournalPageTask persistedTask)
|
|
|
|
|
|
{
|
|
|
|
|
|
var skippedTaskIds = scoreUnit.Questions
|
|
|
|
|
|
.Select(q => q.Task.Id)
|
|
|
|
|
|
.Where(id => id != persistedTask.Id)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
if (skippedTaskIds.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("期刊跨页题评分结果仅保存到主任务,GroupId: {GroupId}, PersistedTaskId: {PersistedTaskId}, SkippedTaskIds: {SkippedTaskIds}",
|
|
|
|
|
|
scoreUnit.GroupId, persistedTask.Id, string.Join(",", skippedTaskIds));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string SerializeAnswerUrls(JournalScoreUnit scoreUnit)
|
|
|
|
|
|
{
|
|
|
|
|
|
var answerUrls = scoreUnit.Questions
|
|
|
|
|
|
.SelectMany(q => q.Question.AnswerUrl ?? [])
|
|
|
|
|
|
.Where(url => !string.IsNullOrWhiteSpace(url))
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
return JsonSerializer.Serialize(answerUrls);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static DateTime GetAnswerStartTime(JournalScoreUnit scoreUnit)
|
|
|
|
|
|
{
|
|
|
|
|
|
var startTimes = scoreUnit.Questions
|
|
|
|
|
|
.Select(q => q.Question.AnswerStartTime)
|
|
|
|
|
|
.Where(t => t != default)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
return startTimes.Count == 0 ? default : startTimes.Min();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static DateTime GetAnswerEndTime(JournalScoreUnit scoreUnit)
|
|
|
|
|
|
{
|
|
|
|
|
|
var endTimes = scoreUnit.Questions
|
|
|
|
|
|
.Select(q => q.Question.AnswerEndTime)
|
|
|
|
|
|
.Where(t => t != default)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
return endTimes.Count == 0 ? default : endTimes.Max();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int GetAnswerSeconds(JournalScoreUnit scoreUnit)
|
|
|
|
|
|
{
|
|
|
|
|
|
return scoreUnit.Questions.Sum(q => Math.Max(0, q.Question.AnswerTime));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int GetBreakCount(JournalScoreUnit scoreUnit)
|
|
|
|
|
|
{
|
|
|
|
|
|
return scoreUnit.Questions.Sum(q => Math.Max(0, q.Question.BreakCount));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string SerializeBreakTimes(JournalScoreUnit scoreUnit)
|
|
|
|
|
|
{
|
|
|
|
|
|
var breakTimes = scoreUnit.Questions
|
|
|
|
|
|
.SelectMany(q => q.Question.BreakTimes ?? [])
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
return JsonSerializer.Serialize(breakTimes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
private static JournalPageTaskUserAnswer BuildAnswerEntity(
|
|
|
|
|
|
QuestionData data,
|
2026-07-02 16:05:13 +08:00
|
|
|
|
JournalScoreUnit scoreUnit,
|
2026-06-30 16:16:12 +08:00
|
|
|
|
Question question,
|
|
|
|
|
|
JournalPageTask task,
|
|
|
|
|
|
JournalPage? page,
|
2026-07-01 14:50:37 +08:00
|
|
|
|
JournalAnswerScoreResult scoreResult,
|
|
|
|
|
|
float completionThreshold)
|
2026-06-30 16:16:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
var growthPoint = Math.Max(0, scoreResult.GrowthPoint);
|
|
|
|
|
|
var points = Math.Max(0, scoreResult.Points);
|
2026-07-01 14:50:37 +08:00
|
|
|
|
var answerStatus = scoreResult.Completion >= completionThreshold
|
|
|
|
|
|
? UserAnswerStatusEnum.Complete
|
|
|
|
|
|
: UserAnswerStatusEnum.Processing;
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
return new JournalPageTaskUserAnswer
|
|
|
|
|
|
{
|
|
|
|
|
|
JournalId = data.JournalId,
|
|
|
|
|
|
JournalPageId = data.PageId,
|
|
|
|
|
|
JournalPageTaskId = task.Id,
|
|
|
|
|
|
JournalPageTaskGroupId = task.GroupId,
|
|
|
|
|
|
UserId = data.UserId,
|
|
|
|
|
|
Result = scoreResult.Result,
|
|
|
|
|
|
Points = points,
|
|
|
|
|
|
GrowthPoints = growthPoint,
|
|
|
|
|
|
Score = Math.Max(0, scoreResult.Score),
|
|
|
|
|
|
Comprehension = Math.Max(0, scoreResult.Comprehension),
|
|
|
|
|
|
Judgment = Math.Max(0, scoreResult.Judgment),
|
|
|
|
|
|
Expression = Math.Max(0, scoreResult.Expression),
|
|
|
|
|
|
Persuasiveness = Math.Max(0, scoreResult.Persuasiveness),
|
|
|
|
|
|
QuestionAnswerUrl = question.Url,
|
2026-07-02 16:05:13 +08:00
|
|
|
|
AnswerUrl = SerializeAnswerUrls(scoreUnit),
|
2026-06-30 16:16:12 +08:00
|
|
|
|
PageAnswerUrl = data.PageAnswerUrl,
|
2026-07-01 09:20:07 +08:00
|
|
|
|
Revision = 0,
|
2026-07-02 16:05:13 +08:00
|
|
|
|
AnswerStartTime = GetAnswerStartTime(scoreUnit),
|
|
|
|
|
|
AnswerEndTime = GetAnswerEndTime(scoreUnit),
|
|
|
|
|
|
AnswerSeconds = GetAnswerSeconds(scoreUnit),
|
2026-06-30 16:16:12 +08:00
|
|
|
|
ImageRecognition = 0,
|
|
|
|
|
|
JournalPageNum = page?.PageNum ?? 0,
|
|
|
|
|
|
Modify = 0,
|
|
|
|
|
|
LastTag = 0,
|
|
|
|
|
|
DotPageNum = page?.PageNum ?? 0,
|
|
|
|
|
|
PageResultUrl = string.Empty,
|
2026-07-08 09:55:17 +08:00
|
|
|
|
Type = ((int)task.Type).ToString(),
|
2026-06-30 16:16:12 +08:00
|
|
|
|
DotPageNo = page?.PageNo ?? string.Empty,
|
|
|
|
|
|
PageAnswerDotUrl = string.Empty,
|
2026-07-02 16:05:13 +08:00
|
|
|
|
BreakCount = GetBreakCount(scoreUnit),
|
|
|
|
|
|
BreakTimes = SerializeBreakTimes(scoreUnit),
|
2026-07-07 10:36:14 +08:00
|
|
|
|
AssignmentStatus = (int)AssignmentStatusEnum.UnAssignmented,
|
2026-07-01 14:50:37 +08:00
|
|
|
|
Status = (int)answerStatus,
|
2026-06-30 16:16:12 +08:00
|
|
|
|
CreatedBy = data.UserId.ToString(),
|
|
|
|
|
|
CreatedAt = data.CreatedTime == default ? now : data.CreatedTime,
|
|
|
|
|
|
UpdatedBy = data.UserId.ToString(),
|
|
|
|
|
|
UpdatedAt = now
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 14:50:37 +08:00
|
|
|
|
private float GetCompletionThreshold()
|
|
|
|
|
|
{
|
|
|
|
|
|
var threshold = configuration.GetValue<float>("AiChat:CompletionThreshold");
|
|
|
|
|
|
if (threshold <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
threshold = DefaultCompletionThreshold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return threshold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
private static string TrimResult(string? result)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(result))
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result.Length <= 50 ? result : result[..50];
|
|
|
|
|
|
}
|
2026-06-11 18:01:07 +08:00
|
|
|
|
}
|
2026-06-29 16:34:26 +08:00
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 期刊题目作答消息
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public class QuestionData
|
|
|
|
|
|
{
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用户ID
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public long UserId { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
2026-07-06 14:21:21 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 学生ID
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public long StudentId { get; set; }
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 期刊ID
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public long JournalId { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
2026-07-06 14:21:21 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 作业ID
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public long HomeworkId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 书籍ID
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public long BookId { get; set; }
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 页ID
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public long PageId { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 页面作答图片地址
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string PageAnswerUrl { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 题目作答列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public Question[] Questions { get; set; } = [];
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建时间
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public DateTime CreatedTime { get; set; }
|
2026-07-06 14:21:21 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 兼容新版作答消息字段
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Normalize()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (UserId <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = StudentId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (JournalId <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
JournalId = BookId > 0 ? BookId : HomeworkId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-29 16:34:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 题目作答数据
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public class Question
|
|
|
|
|
|
{
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 题目ID
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public long Id { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 题目图片地址
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string Url { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 答案图片地址
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string[] AnswerUrl { get; set; } = [];
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 作答开始时间
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public DateTime AnswerStartTime { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 作答结束时间
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public DateTime AnswerEndTime { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 作答耗时秒数
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public int AnswerTime { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 中断次数
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public int BreakCount { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 中断记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public List<BreakTime> BreakTimes { get; set; } = [];
|
2026-06-29 16:34:26 +08:00
|
|
|
|
}
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 中断时间记录
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public class BreakTime
|
|
|
|
|
|
{
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 中断时间
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public DateTime Time { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 等待时间
|
|
|
|
|
|
/// </summary>
|
2026-06-29 16:34:26 +08:00
|
|
|
|
public long WaitTime { get; set; }
|
2026-06-30 16:16:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// AI评分结果
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class JournalAnswerScoreResult
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 题目得分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Score { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 成长值
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public int GrowthPoint { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 积分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public int Points { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 理解力评分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Comprehension { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 判断力评分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Judgment { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 表达力评分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Expression { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 说服力评分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Persuasiveness { get; set; }
|
|
|
|
|
|
|
2026-07-01 14:50:37 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 瀹屾垚搴?
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Completion { get; set; }
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 50字内评语
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string Result { get; set; } = string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 14:50:37 +08:00
|
|
|
|
public record JournalAnswerContext(
|
|
|
|
|
|
JournalPageTaskUserAnswer Answer,
|
|
|
|
|
|
Question Question,
|
|
|
|
|
|
JournalPageTask Task,
|
|
|
|
|
|
JournalAnswerScoreResult ScoreResult);
|
|
|
|
|
|
|
2026-07-02 16:05:13 +08:00
|
|
|
|
public record JournalQuestionContext(
|
|
|
|
|
|
Question Question,
|
|
|
|
|
|
JournalPageTask Task,
|
|
|
|
|
|
JournalPage? Page);
|
|
|
|
|
|
|
|
|
|
|
|
public record JournalScoreUnit(
|
|
|
|
|
|
long GroupId,
|
|
|
|
|
|
List<JournalQuestionContext> Questions);
|
|
|
|
|
|
|
2026-06-30 16:16:12 +08:00
|
|
|
|
public record AnswerImageContent(string DataUrl);
|
2026-07-02 16:05:13 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// AI评分不可重试异常。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class NonRetryAiScoreException(string message) : Exception(message);
|