feat: 新增上传地址管理、答题评分及社区消息功能
1. 新增UploadDomain实体与上传地址分配逻辑,为用户分配可用上传域名 2. 新增绑定期刊消息推送,通过RabbitMQ传递绑定信息 3. 优化答题评分逻辑,新增完成度阈值判定与社区消息插入 4. 新增配置项用于评分阈值配置 5. 补充相关DTO与实体类字段,完善数据传输与存储
This commit is contained in:
@ -59,6 +59,37 @@ public class BindJournalOutput
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户绑定期刊消息
|
||||
/// </summary>
|
||||
public class BindJournalMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户作答信息上传地址
|
||||
/// </summary>
|
||||
public string UploadDomain { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户期刊关联查询输入DTO
|
||||
/// </summary>
|
||||
|
||||
@ -55,6 +55,11 @@ public class UsersOutput
|
||||
/// 当前成长值
|
||||
/// </summary>
|
||||
public int GrowthPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户作答信息上传地址
|
||||
/// </summary>
|
||||
public string UploadDomain { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -146,6 +146,11 @@ public class WxUserOutput
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户作答信息上传地址
|
||||
/// </summary>
|
||||
public string UploadDomain { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
25
QYZH.InteractiveMagazine.Models/Entity/UploadDomain.cs
Normal file
25
QYZH.InteractiveMagazine.Models/Entity/UploadDomain.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传地址管理表
|
||||
/// </summary>
|
||||
[SugarTable("UploadDomain")]
|
||||
public partial class UploadDomain : SqlSugarBaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Desc:上传地址
|
||||
/// Default:
|
||||
/// Nullable:False
|
||||
/// </summary>
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Desc:已分配人数
|
||||
/// Default:0
|
||||
/// Nullable:False
|
||||
/// </summary>
|
||||
public int AssignedCount { get; set; }
|
||||
}
|
||||
}
|
||||
@ -57,5 +57,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
||||
/// Nullable:False
|
||||
/// </summary>
|
||||
public bool IsLastOnline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:用户作答信息上传地址
|
||||
/// Default:
|
||||
/// Nullable:False
|
||||
/// </summary>
|
||||
public string UploadDomain { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
@ -17,9 +18,14 @@ public class UserJournalService(
|
||||
BaseRepository<Users> usersRepository,
|
||||
BaseRepository<Journal> journalRepository,
|
||||
ILogger<UserJournalService> logger,
|
||||
IRabbitMQService rabbitMqService,
|
||||
IPetService petService)
|
||||
: BaseRepository<UserJournal>, IUserJournalService
|
||||
{
|
||||
private const string JournalExchange = "ex.journal";
|
||||
private const string BindJournalQueue = "mq.journal.bindUser";
|
||||
private const string BindJournalRoutingKey = "rk.journal.bindUser";
|
||||
|
||||
/// <summary>
|
||||
/// 用户绑定期刊(扫码绑定)
|
||||
/// </summary>
|
||||
@ -93,6 +99,7 @@ public class UserJournalService(
|
||||
}
|
||||
|
||||
logger.LogInformation("用户绑定期刊成功,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, userJournal.Id);
|
||||
await SendBindJournalMessageAsync(user, journal);
|
||||
|
||||
// 首次绑定期刊时激活宠物
|
||||
if (isFirstBind)
|
||||
@ -119,6 +126,36 @@ public class UserJournalService(
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SendBindJournalMessageAsync(Users user, Journal journal)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
|
||||
{
|
||||
Exchange = JournalExchange,
|
||||
Queue = BindJournalQueue,
|
||||
RoutingKey = BindJournalRoutingKey,
|
||||
Data = new BindJournalMessage
|
||||
{
|
||||
UserId = user.Id,
|
||||
JournalId = journal.Id,
|
||||
StartTime = journal.StartTime,
|
||||
EndTime = journal.EndTime,
|
||||
UploadDomain = user.UploadDomain
|
||||
}
|
||||
});
|
||||
|
||||
if (!messageSent)
|
||||
{
|
||||
logger.LogError("发送绑定期刊消息失败,UserId: {UserId}, JournalId: {JournalId}", user.Id, journal.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "发送绑定期刊消息异常,UserId: {UserId}, JournalId: {JournalId}", user.Id, journal.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户的期刊绑定列表
|
||||
/// </summary>
|
||||
|
||||
@ -19,6 +19,7 @@ namespace QYZH.InteractiveMagazine.Service;
|
||||
/// </summary>
|
||||
public class WeChatAuthService(
|
||||
BaseRepository<WxUser> wxUserRepository,
|
||||
BaseRepository<UploadDomain> uploadDomainRepository,
|
||||
IConfiguration configuration,
|
||||
ILogger<WeChatAuthService> logger,
|
||||
IPetService petService)
|
||||
@ -260,7 +261,27 @@ public class WeChatAuthService(
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
await wxUserRepository.Context.Insertable(newUser).ExecuteReturnIdentityAsync();
|
||||
await UseTranAsync(async () =>
|
||||
{
|
||||
var uploadDomain = await uploadDomainRepository.Queryable()
|
||||
.Where(d => d.Status == 1 && !d.IsDeleted)
|
||||
.OrderBy(d => d.AssignedCount, SqlSugar.OrderByType.Asc)
|
||||
.OrderBy(d => d.Id, SqlSugar.OrderByType.Asc)
|
||||
.FirstAsync();
|
||||
|
||||
BusinessException.ThrowIf(uploadDomain == null, "暂无可用上传地址,请联系管理员", ResultCode.UNPROCESSABLE_ENTITY);
|
||||
|
||||
newUser.UploadDomain = uploadDomain!.Domain;
|
||||
|
||||
await wxUserRepository.Context.Insertable(newUser).ExecuteCommandAsync();
|
||||
|
||||
await uploadDomainRepository.Updateable()
|
||||
.SetColumns(d => d.AssignedCount == d.AssignedCount + 1)
|
||||
.SetColumns(d => d.UpdatedBy == wxUserId.ToString())
|
||||
.SetColumns(d => d.UpdatedAt == DateTime.Now)
|
||||
.Where(d => d.Id == uploadDomain.Id)
|
||||
.ExecuteCommandAsync();
|
||||
});
|
||||
|
||||
logger.LogInformation("新用户创建成功,UserId: {UserId}, WxUserId: {WxUserId}, Name: {Name}",
|
||||
newUser.Id, wxUserId, input.Name);
|
||||
@ -431,7 +452,8 @@ public class WeChatAuthService(
|
||||
Type = user.Type.ToString(),
|
||||
Status = user.Status.ToString(),
|
||||
IsLastOnline = user.IsLastOnline,
|
||||
CreatedAt = user.CreatedAt
|
||||
CreatedAt = user.CreatedAt,
|
||||
UploadDomain = user.UploadDomain
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,14 @@ public class JournalController : WeChatBaseController
|
||||
private readonly IUserJournalService _userJournalService;
|
||||
private readonly ILogger<JournalController> _logger;
|
||||
|
||||
public JournalController(IUserJournalService userJournalService, ILogger<JournalController> logger)
|
||||
/// <summary>
|
||||
/// 初始化小程序期刊控制器
|
||||
/// </summary>
|
||||
/// <param name="userJournalService">用户期刊关联服务</param>
|
||||
/// <param name="logger">日志服务</param>
|
||||
public JournalController(
|
||||
IUserJournalService userJournalService,
|
||||
ILogger<JournalController> logger)
|
||||
{
|
||||
_userJournalService = userJournalService;
|
||||
_logger = logger;
|
||||
|
||||
@ -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,9 +108,9 @@ 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;
|
||||
@ -118,6 +125,9 @@ public class JournalTaskReceiveConsumer(
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await InsertCommunityMessageIfNeededAsync(client, context, cancellationToken);
|
||||
}
|
||||
|
||||
client.Ado.CommitTran();
|
||||
}
|
||||
catch
|
||||
@ -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