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": {