This commit is contained in:
2026-07-01 15:04:45 +08:00
10 changed files with 262 additions and 25 deletions

View File

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

View File

@ -55,6 +55,11 @@ public class UsersOutput
/// 当前成长值
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// 用户作答信息上传地址
/// </summary>
public string UploadDomain { get; set; } = string.Empty;
}
/// <summary>

View File

@ -146,6 +146,11 @@ public class WxUserOutput
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 用户作答信息上传地址
/// </summary>
public string UploadDomain { get; set; } = string.Empty;
}
/// <summary>

View 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; }
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

@ -58,7 +58,9 @@
"Temperature": 0.5,
"ScoreMaxRetryCount": 3,
"ScoreRetryDelayMilliseconds": 1000,
"MaxImageBytes": 10485760
"MaxImageBytes": 10485760,
"CompletionThreshold": 80,
"CommunityScoreThreshold": 90
},
"AllowedHosts": "*",
"AliyunOSSConfigs": {