feat: 新增上传地址管理、答题评分及社区消息功能
1. 新增UploadDomain实体与上传地址分配逻辑,为用户分配可用上传域名 2. 新增绑定期刊消息推送,通过RabbitMQ传递绑定信息 3. 优化答题评分逻辑,新增完成度阈值判定与社区消息插入 4. 新增配置项用于评分阈值配置 5. 补充相关DTO与实体类字段,完善数据传输与存储
This commit is contained in:
@ -22,6 +22,8 @@ public class JournalTaskReceiveConsumer(
|
||||
private const int DefaultAiScoreMaxRetryCount = 3;
|
||||
private const int DefaultAiScoreRetryDelayMilliseconds = 1000;
|
||||
private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
|
||||
private const float DefaultCompletionThreshold = 80;
|
||||
private const float DefaultCommunityScoreThreshold = 90;
|
||||
|
||||
public string Exchange => "ex.journal";
|
||||
|
||||
@ -63,7 +65,7 @@ public class JournalTaskReceiveConsumer(
|
||||
.Where(p => p.Id == data.PageId && !p.IsDeleted)
|
||||
.FirstAsync(cancellationToken);
|
||||
|
||||
var answerEntities = new List<JournalPageTaskUserAnswer>();
|
||||
var answerContexts = new List<JournalAnswerContext>();
|
||||
foreach (var question in data.Questions)
|
||||
{
|
||||
if (!taskMap.TryGetValue(question.Id, out var task))
|
||||
@ -80,10 +82,14 @@ public class JournalTaskReceiveConsumer(
|
||||
|
||||
referenceAnswerMap.TryGetValue(task.Id, out var taskReferenceAnswers);
|
||||
var scoreResult = await ScoreQuestionAsync(task, question, taskReferenceAnswers ?? [], cancellationToken);
|
||||
answerEntities.Add(BuildAnswerEntity(data, question, task, page, scoreResult));
|
||||
answerContexts.Add(new JournalAnswerContext(
|
||||
BuildAnswerEntity(data, question, task, page, scoreResult, GetCompletionThreshold()),
|
||||
question,
|
||||
task,
|
||||
scoreResult));
|
||||
}
|
||||
|
||||
if (answerEntities.Count == 0)
|
||||
if (answerContexts.Count == 0)
|
||||
{
|
||||
logger.LogWarning("期刊任务消息没有可入库的答题记录,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}", data.UserId, data.JournalId, data.PageId);
|
||||
return;
|
||||
@ -92,8 +98,9 @@ public class JournalTaskReceiveConsumer(
|
||||
client.Ado.BeginTran();
|
||||
try
|
||||
{
|
||||
foreach (var answer in answerEntities)
|
||||
foreach (var context in answerContexts)
|
||||
{
|
||||
var answer = context.Answer;
|
||||
var existing = await client.Queryable<JournalPageTaskUserAnswer>()
|
||||
.Where(a => a.UserId == answer.UserId && a.JournalPageTaskId == answer.JournalPageTaskId && !a.IsDeleted)
|
||||
.FirstAsync(cancellationToken);
|
||||
@ -101,21 +108,24 @@ public class JournalTaskReceiveConsumer(
|
||||
if (existing == null)
|
||||
{
|
||||
await client.Insertable(answer).ExecuteCommandAsync(cancellationToken);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
await InsertCommunityMessageIfNeededAsync(client, context, cancellationToken);
|
||||
}
|
||||
|
||||
client.Ado.CommitTran();
|
||||
@ -127,7 +137,7 @@ public class JournalTaskReceiveConsumer(
|
||||
}
|
||||
|
||||
logger.LogInformation("期刊任务答题记录保存完成,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}, Count: {Count}",
|
||||
data.UserId, data.JournalId, data.PageId, answerEntities.Count);
|
||||
data.UserId, data.JournalId, data.PageId, answerContexts.Count);
|
||||
}
|
||||
|
||||
public Task OnErrorAsync(byte[] body, Exception exception)
|
||||
@ -293,6 +303,7 @@ public class JournalTaskReceiveConsumer(
|
||||
prompt.AppendLine(" \"Judgment\": 0,");
|
||||
prompt.AppendLine(" \"Expression\": 0,");
|
||||
prompt.AppendLine(" \"Persuasiveness\": 0,");
|
||||
prompt.AppendLine(" \"Completion\": 100,");
|
||||
prompt.AppendLine(" \"Result\": \"50字内的中文评语\"");
|
||||
prompt.AppendLine("}");
|
||||
return prompt.ToString();
|
||||
@ -566,11 +577,15 @@ public class JournalTaskReceiveConsumer(
|
||||
Question question,
|
||||
JournalPageTask task,
|
||||
JournalPage? page,
|
||||
JournalAnswerScoreResult scoreResult)
|
||||
JournalAnswerScoreResult scoreResult,
|
||||
float completionThreshold)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var growthPoint = Math.Max(0, scoreResult.GrowthPoint);
|
||||
var points = Math.Max(0, scoreResult.Points);
|
||||
var answerStatus = scoreResult.Completion >= completionThreshold
|
||||
? UserAnswerStatusEnum.Complete
|
||||
: UserAnswerStatusEnum.Processing;
|
||||
|
||||
return new JournalPageTaskUserAnswer
|
||||
{
|
||||
@ -605,8 +620,8 @@ public class JournalTaskReceiveConsumer(
|
||||
PageAnswerDotUrl = string.Empty,
|
||||
BreakCount = question.BreakCount,
|
||||
BreakTimes = JsonSerializer.Serialize(question.BreakTimes ?? []),
|
||||
AssignmentStatus = UserAnswerStatusEnum.Complete.ToString(),
|
||||
Status = (int)UserAnswerStatusEnum.Complete,
|
||||
AssignmentStatus = answerStatus.ToString(),
|
||||
Status = (int)answerStatus,
|
||||
CreatedBy = data.UserId.ToString(),
|
||||
CreatedAt = data.CreatedTime == default ? now : data.CreatedTime,
|
||||
UpdatedBy = data.UserId.ToString(),
|
||||
@ -660,6 +675,76 @@ public class JournalTaskReceiveConsumer(
|
||||
};
|
||||
}
|
||||
|
||||
private async Task InsertCommunityMessageIfNeededAsync(
|
||||
ISqlSugarClient client,
|
||||
JournalAnswerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scoreThreshold = configuration.GetValue<float>("AiChat:CommunityScoreThreshold");
|
||||
if (scoreThreshold <= 0)
|
||||
{
|
||||
scoreThreshold = DefaultCommunityScoreThreshold;
|
||||
}
|
||||
|
||||
if (context.Answer.Status != (int)UserAnswerStatusEnum.Complete || context.ScoreResult.Score < scoreThreshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var exists = await client.Queryable<CommunityMessage>()
|
||||
.Where(m => m.JournalTaskAnswerId == context.Answer.Id && !m.IsDeleted)
|
||||
.AnyAsync(cancellationToken);
|
||||
if (exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userJournal = await client.Queryable<UserJournal>()
|
||||
.Where(uj => uj.UserId == context.Answer.UserId && uj.JournalId == context.Answer.JournalId && !uj.IsDeleted)
|
||||
.FirstAsync(cancellationToken);
|
||||
if (userJournal == null)
|
||||
{
|
||||
logger.LogWarning("高分答案未找到用户期刊关系,跳过社区消息写入,UserId: {UserId}, JournalId: {JournalId}, AnswerId: {AnswerId}",
|
||||
context.Answer.UserId, context.Answer.JournalId, context.Answer.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var communityMessage = new CommunityMessage
|
||||
{
|
||||
JournalId = context.Answer.JournalId,
|
||||
UserId = context.Answer.UserId,
|
||||
UserJournalId = userJournal.Id,
|
||||
JournalTaskId = context.Answer.JournalPageTaskId,
|
||||
JournalTaskAnswerId = context.Answer.Id,
|
||||
Content = context.ScoreResult.Result,
|
||||
ImageUrl = context.Question.AnswerUrl.FirstOrDefault() ?? string.Empty,
|
||||
SortOrder = 0,
|
||||
IsActive = true,
|
||||
Type = MessageTypeEnum.Message,
|
||||
LikeCount = 0,
|
||||
IsFeatured = 0,
|
||||
Status = 1,
|
||||
CreatedBy = context.Answer.UserId.ToString(),
|
||||
CreatedAt = now,
|
||||
UpdatedBy = context.Answer.UserId.ToString(),
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
await client.Insertable(communityMessage).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private float GetCompletionThreshold()
|
||||
{
|
||||
var threshold = configuration.GetValue<float>("AiChat:CompletionThreshold");
|
||||
if (threshold <= 0)
|
||||
{
|
||||
threshold = DefaultCompletionThreshold;
|
||||
}
|
||||
|
||||
return threshold;
|
||||
}
|
||||
|
||||
private static string TrimResult(string? result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
@ -809,10 +894,21 @@ public class JournalAnswerScoreResult
|
||||
/// </summary>
|
||||
public float Persuasiveness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 瀹屾垚搴?
|
||||
/// </summary>
|
||||
public float Completion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 50字内评语
|
||||
/// </summary>
|
||||
public string Result { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record JournalAnswerContext(
|
||||
JournalPageTaskUserAnswer Answer,
|
||||
Question Question,
|
||||
JournalPageTask Task,
|
||||
JournalAnswerScoreResult ScoreResult);
|
||||
|
||||
public record AnswerImageContent(string DataUrl);
|
||||
|
||||
@ -58,7 +58,9 @@
|
||||
"Temperature": 0.5,
|
||||
"ScoreMaxRetryCount": 3,
|
||||
"ScoreRetryDelayMilliseconds": 1000,
|
||||
"MaxImageBytes": 10485760
|
||||
"MaxImageBytes": 10485760,
|
||||
"CompletionThreshold": 80,
|
||||
"CommunityScoreThreshold": 90
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AliyunOSSConfigs": {
|
||||
|
||||
Reference in New Issue
Block a user