feat: 新增上传地址管理、答题评分及社区消息功能

1. 新增UploadDomain实体与上传地址分配逻辑,为用户分配可用上传域名
2. 新增绑定期刊消息推送,通过RabbitMQ传递绑定信息
3. 优化答题评分逻辑,新增完成度阈值判定与社区消息插入
4. 新增配置项用于评分阈值配置
5. 补充相关DTO与实体类字段,完善数据传输与存储
This commit is contained in:
glz
2026-07-01 14:50:37 +08:00
parent eb4f09e391
commit 07614b5fe6
10 changed files with 262 additions and 25 deletions

View File

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