From 07614b5fe6f143853e379960d13303239bdb7629 Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Wed, 1 Jul 2026 14:50:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E7=AE=A1=E7=90=86=E3=80=81=E7=AD=94=E9=A2=98?= =?UTF-8?q?=E8=AF=84=E5=88=86=E5=8F=8A=E7=A4=BE=E5=8C=BA=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增UploadDomain实体与上传地址分配逻辑,为用户分配可用上传域名 2. 新增绑定期刊消息推送,通过RabbitMQ传递绑定信息 3. 优化答题评分逻辑,新增完成度阈值判定与社区消息插入 4. 新增配置项用于评分阈值配置 5. 补充相关DTO与实体类字段,完善数据传输与存储 --- .../Dto/Journal/BindJournalDto.cs | 31 ++++ .../Dto/UsersDto.cs | 5 + .../Dto/WeChat/WeChatDto.cs | 5 + .../Entity/UploadDomain.cs | 25 ++++ .../Entity/Users.cs | 7 + .../UserJournalService.cs | 37 +++++ .../WeChatAuthService.cs | 26 +++- .../Controllers/WeChat/JournalController.cs | 9 +- .../Consumers/JournalTaskReceiveConsumer.cs | 138 +++++++++++++++--- .../appsettings.json | 4 +- 10 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 QYZH.InteractiveMagazine.Models/Entity/UploadDomain.cs diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs index 12ef5ad..7862518 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs @@ -59,6 +59,37 @@ public class BindJournalOutput public DateTime CreatedAt { get; set; } } +/// +/// 用户绑定期刊消息 +/// +public class BindJournalMessage +{ + /// + /// 用户Id + /// + public long UserId { get; set; } + + /// + /// 期刊Id + /// + public long JournalId { get; set; } + + /// + /// 期刊开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 期刊结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 用户作答信息上传地址 + /// + public string UploadDomain { get; set; } = string.Empty; +} + /// /// 用户期刊关联查询输入DTO /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs b/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs index 101178c..2613141 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs @@ -55,6 +55,11 @@ public class UsersOutput /// 当前成长值 /// public int GrowthPoints { get; set; } + + /// + /// 用户作答信息上传地址 + /// + public string UploadDomain { get; set; } = string.Empty; } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs b/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs index 0d45043..2f61153 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs @@ -146,6 +146,11 @@ public class WxUserOutput /// 创建时间 /// public DateTime CreatedAt { get; set; } + + /// + /// 用户作答信息上传地址 + /// + public string UploadDomain { get; set; } = string.Empty; } /// diff --git a/QYZH.InteractiveMagazine.Models/Entity/UploadDomain.cs b/QYZH.InteractiveMagazine.Models/Entity/UploadDomain.cs new file mode 100644 index 0000000..7487440 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/UploadDomain.cs @@ -0,0 +1,25 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity +{ + /// + /// 上传地址管理表 + /// + [SugarTable("UploadDomain")] + public partial class UploadDomain : SqlSugarBaseEntity + { + /// + /// Desc:上传地址 + /// Default: + /// Nullable:False + /// + public string Domain { get; set; } = string.Empty; + + /// + /// Desc:已分配人数 + /// Default:0 + /// Nullable:False + /// + public int AssignedCount { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/Users.cs b/QYZH.InteractiveMagazine.Models/Entity/Users.cs index abda0c0..f5e4c3c 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/Users.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/Users.cs @@ -57,5 +57,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Nullable:False /// public bool IsLastOnline { get; set; } + + /// + /// Desc:用户作答信息上传地址 + /// Default: + /// Nullable:False + /// + public string UploadDomain { get; set; } = string.Empty; } } diff --git a/QYZH.InteractiveMagazine.Service/UserJournalService.cs b/QYZH.InteractiveMagazine.Service/UserJournalService.cs index f69a97b..2a16225 100644 --- a/QYZH.InteractiveMagazine.Service/UserJournalService.cs +++ b/QYZH.InteractiveMagazine.Service/UserJournalService.cs @@ -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 usersRepository, BaseRepository journalRepository, ILogger logger, + IRabbitMQService rabbitMqService, IPetService petService) : BaseRepository, IUserJournalService { + private const string JournalExchange = "ex.journal"; + private const string BindJournalQueue = "mq.journal.bindUser"; + private const string BindJournalRoutingKey = "rk.journal.bindUser"; + /// /// 用户绑定期刊(扫码绑定) /// @@ -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); + } + } + /// /// 获取用户的期刊绑定列表 /// diff --git a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs index a6d5498..d69f7b2 100644 --- a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs +++ b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs @@ -19,6 +19,7 @@ namespace QYZH.InteractiveMagazine.Service; /// public class WeChatAuthService( BaseRepository wxUserRepository, + BaseRepository uploadDomainRepository, IConfiguration configuration, ILogger 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 }; } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs index 0292c27..44e260e 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs @@ -13,7 +13,14 @@ public class JournalController : WeChatBaseController private readonly IUserJournalService _userJournalService; private readonly ILogger _logger; - public JournalController(IUserJournalService userJournalService, ILogger logger) + /// + /// 初始化小程序期刊控制器 + /// + /// 用户期刊关联服务 + /// 日志服务 + public JournalController( + IUserJournalService userJournalService, + ILogger logger) { _userJournalService = userJournalService; _logger = logger; diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs index 3c378af..6152a7d 100644 --- a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs +++ b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs @@ -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(); + var answerContexts = new List(); 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() .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("AiChat:CommunityScoreThreshold"); + if (scoreThreshold <= 0) + { + scoreThreshold = DefaultCommunityScoreThreshold; + } + + if (context.Answer.Status != (int)UserAnswerStatusEnum.Complete || context.ScoreResult.Score < scoreThreshold) + { + return; + } + + var exists = await client.Queryable() + .Where(m => m.JournalTaskAnswerId == context.Answer.Id && !m.IsDeleted) + .AnyAsync(cancellationToken); + if (exists) + { + return; + } + + var userJournal = await client.Queryable() + .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("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 /// public float Persuasiveness { get; set; } + /// + /// 瀹屾垚搴? + /// + public float Completion { get; set; } + /// /// 50字内评语 /// public string Result { get; set; } = string.Empty; } +public record JournalAnswerContext( + JournalPageTaskUserAnswer Answer, + Question Question, + JournalPageTask Task, + JournalAnswerScoreResult ScoreResult); + public record AnswerImageContent(string DataUrl); diff --git a/QYZH.InteractiveMagazine.WorkService/appsettings.json b/QYZH.InteractiveMagazine.WorkService/appsettings.json index 5658ea6..3585c67 100644 --- a/QYZH.InteractiveMagazine.WorkService/appsettings.json +++ b/QYZH.InteractiveMagazine.WorkService/appsettings.json @@ -58,7 +58,9 @@ "Temperature": 0.5, "ScoreMaxRetryCount": 3, "ScoreRetryDelayMilliseconds": 1000, - "MaxImageBytes": 10485760 + "MaxImageBytes": 10485760, + "CompletionThreshold": 80, + "CommunityScoreThreshold": 90 }, "AllowedHosts": "*", "AliyunOSSConfigs": {