mi-assessment/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs
2026-02-24 13:39:51 +08:00

682 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Models.Assessment;
using MiAssessment.Model.Models.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 测评服务实现
/// </summary>
/// <remarks>
/// 提供测评模块的核心业务功能,包括:
/// - 测评介绍获取
/// - 题目列表查询
/// - 答案提交处理
/// - 报告状态查询
/// - 测评结果获取
/// - 邀请码验证
/// - 往期测评记录查询
/// </remarks>
public class AssessmentService : IAssessmentService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<AssessmentService> _logger;
private readonly ReportGenerationService _reportGenerationService;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="logger">日志记录器</param>
/// <param name="reportGenerationService">报告生成服务</param>
public AssessmentService(
MiAssessmentDbContext dbContext,
ILogger<AssessmentService> logger,
ReportGenerationService reportGenerationService)
{
_dbContext = dbContext;
_logger = logger;
_reportGenerationService = reportGenerationService;
}
/// <inheritdoc />
public async Task<AssessmentIntroDto?> GetIntroAsync(long typeId)
{
_logger.LogDebug("获取测评介绍typeId: {TypeId}", typeId);
var assessmentType = await _dbContext.AssessmentTypes
.AsNoTracking()
.Where(a => a.Id == typeId && !a.IsDeleted)
.Select(a => new AssessmentIntroDto
{
ImageUrl = a.DetailImageUrl,
Content = a.IntroContent ?? string.Empty,
ContentType = "html",
Price = a.Price
})
.FirstOrDefaultAsync();
if (assessmentType == null)
{
_logger.LogWarning("测评类型不存在typeId: {TypeId}", typeId);
}
else
{
_logger.LogDebug("获取测评介绍成功typeId: {TypeId}", typeId);
}
return assessmentType;
}
/// <inheritdoc />
public async Task<List<QuestionDto>> GetQuestionListAsync(long typeId)
{
_logger.LogDebug("获取题目列表typeId: {TypeId}", typeId);
var questions = await _dbContext.Questions
.AsNoTracking()
.Where(q => q.AssessmentTypeId == typeId && q.Status == 1 && !q.IsDeleted)
.OrderBy(q => q.QuestionNo)
.Select(q => new QuestionDto
{
Id = q.Id,
QuestionNo = q.QuestionNo,
Content = q.Content
})
.ToListAsync();
_logger.LogDebug("获取到 {Count} 道题目typeId: {TypeId}", questions.Count, typeId);
return questions;
}
/// <inheritdoc />
public async Task<ResultStatusDto?> GetResultStatusAsync(long userId, long recordId)
{
_logger.LogDebug("查询报告状态userId: {UserId}, recordId: {RecordId}", userId, recordId);
var record = await _dbContext.AssessmentRecords
.AsNoTracking()
.Where(r => r.Id == recordId && r.UserId == userId && !r.IsDeleted)
.Select(r => new ResultStatusDto
{
Status = r.Status,
IsCompleted = r.Status == 4
})
.FirstOrDefaultAsync();
if (record == null)
{
_logger.LogWarning("测评记录不存在或不属于当前用户userId: {UserId}, recordId: {RecordId}", userId, recordId);
}
else
{
_logger.LogDebug("查询报告状态成功status: {Status}, isCompleted: {IsCompleted}", record.Status, record.IsCompleted);
}
return record;
}
/// <inheritdoc />
public async Task<PagedResult<AssessmentHistoryDto>> GetHistoryListAsync(long userId, int page, int pageSize)
{
_logger.LogDebug("获取往期测评列表userId: {UserId}, page: {Page}, pageSize: {PageSize}", userId, page, pageSize);
// 确保分页参数有效
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 20;
if (pageSize > 100) pageSize = 100;
var query = _dbContext.AssessmentRecords
.AsNoTracking()
.Where(r => r.UserId == userId && !r.IsDeleted);
// 获取总数
var total = await query.CountAsync();
// 分页查询(使用左连接避免缺少关联数据时丢失记录)
var records = await query
.OrderByDescending(r => r.CreateTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.GroupJoin(
_dbContext.Orders.AsNoTracking(),
r => r.OrderId,
o => o.Id,
(r, orders) => new { Record = r, Orders = orders }
)
.SelectMany(
ro => ro.Orders.DefaultIfEmpty(),
(ro, o) => new { ro.Record, Order = o }
)
.GroupJoin(
_dbContext.AssessmentTypes.AsNoTracking(),
ro => ro.Record.AssessmentTypeId,
at => at.Id,
(ro, types) => new { ro.Record, ro.Order, Types = types }
)
.SelectMany(
rot => rot.Types.DefaultIfEmpty(),
(rot, at) => new AssessmentHistoryDto
{
Id = rot.Record.Id,
OrderNo = rot.Order != null ? rot.Order.OrderNo : "",
AssessmentName = at != null ? at.Name : "多元智能测评",
Name = rot.Record.Name,
AssessmentTypeId = rot.Record.AssessmentTypeId,
Status = rot.Record.Status,
StatusText = GetStatusText(rot.Record.Status),
TestDate = rot.Record.CreateTime.ToString("yyyy-MM-dd")
}
)
.ToListAsync();
_logger.LogDebug("获取到 {Count} 条往期测评记录,总数: {Total}", records.Count, total);
return PagedResult<AssessmentHistoryDto>.Create(records, total, page, pageSize);
}
/// <inheritdoc />
public async Task<SubmitAnswersResponse> SubmitAnswersAsync(long userId, SubmitAnswersRequest request)
{
_logger.LogDebug("提交测评答案userId: {UserId}, recordId: {RecordId}, answerCount: {AnswerCount}",
userId, request.RecordId, request.Answers?.Count ?? 0);
// 参数验证
if (request.Answers == null || request.Answers.Count == 0)
{
_logger.LogWarning("答案列表为空userId: {UserId}, recordId: {RecordId}", userId, request.RecordId);
throw new InvalidOperationException("答案列表不能为空");
}
// 查询测评记录
var record = await _dbContext.AssessmentRecords
.Where(r => r.Id == request.RecordId && !r.IsDeleted)
.FirstOrDefaultAsync();
// 验证记录是否存在
if (record == null)
{
_logger.LogWarning("测评记录不存在recordId: {RecordId}", request.RecordId);
throw new InvalidOperationException("测评记录不存在");
}
// 验证记录归属当前用户
if (record.UserId != userId)
{
_logger.LogWarning("测评记录不属于当前用户userId: {UserId}, recordUserId: {RecordUserId}, recordId: {RecordId}",
userId, record.UserId, request.RecordId);
throw new UnauthorizedAccessException("无权限访问该测评记录");
}
// 验证记录状态(只有待测评或测评中状态才能提交答案)
if (record.Status != 1 && record.Status != 2)
{
_logger.LogWarning("测评记录状态不允许提交答案status: {Status}, recordId: {RecordId}",
record.Status, request.RecordId);
throw new InvalidOperationException("当前测评状态不允许提交答案");
}
// 获取该测评类型的题目数量
var questionCount = await _dbContext.Questions
.Where(q => q.AssessmentTypeId == record.AssessmentTypeId && q.Status == 1 && !q.IsDeleted)
.CountAsync();
// 验证答案数量与题目数量匹配
if (request.Answers.Count != questionCount)
{
_logger.LogWarning("答案数量与题目数量不匹配answerCount: {AnswerCount}, questionCount: {QuestionCount}, recordId: {RecordId}",
request.Answers.Count, questionCount, request.RecordId);
throw new InvalidOperationException($"答案数量({request.Answers.Count})与题目数量({questionCount})不匹配");
}
// 获取所有有效题目ID用于验证
var validQuestionIds = await _dbContext.Questions
.Where(q => q.AssessmentTypeId == record.AssessmentTypeId && q.Status == 1 && !q.IsDeleted)
.Select(q => new { q.Id, q.QuestionNo })
.ToDictionaryAsync(q => q.Id, q => q.QuestionNo);
// 验证所有答案的题目ID是否有效
foreach (var answer in request.Answers)
{
if (!validQuestionIds.ContainsKey(answer.QuestionId))
{
_logger.LogWarning("无效的题目IDquestionId: {QuestionId}, recordId: {RecordId}",
answer.QuestionId, request.RecordId);
throw new InvalidOperationException($"无效的题目ID: {answer.QuestionId}");
}
}
// 使用事务保存答案
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 删除已有的答案(如果有的话,支持重新提交)
var existingAnswers = await _dbContext.AssessmentAnswers
.Where(a => a.RecordId == request.RecordId)
.ToListAsync();
if (existingAnswers.Any())
{
_dbContext.AssessmentAnswers.RemoveRange(existingAnswers);
_logger.LogDebug("删除已有答案count: {Count}, recordId: {RecordId}",
existingAnswers.Count, request.RecordId);
}
// 创建新的答案记录
var now = DateTime.Now;
var answers = request.Answers.Select(a => new MiAssessment.Model.Entities.AssessmentAnswer
{
RecordId = request.RecordId,
QuestionId = a.QuestionId,
QuestionNo = validQuestionIds[a.QuestionId],
AnswerValue = a.AnswerValue,
CreateTime = now
}).ToList();
await _dbContext.AssessmentAnswers.AddRangeAsync(answers);
// 更新测评记录状态为"生成中"
record.Status = 3; // 3=生成中
record.SubmitTime = now;
record.UpdateTime = now;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("测评答案提交成功userId: {UserId}, recordId: {RecordId}, answerCount: {AnswerCount}",
userId, request.RecordId, answers.Count);
// 触发报告生成
try
{
await _reportGenerationService.GenerateReportAsync(request.RecordId);
}
catch (Exception ex)
{
_logger.LogError(ex, "报告生成失败recordId: {RecordId}", request.RecordId);
// 报告生成失败不影响答案提交的返回状态保持为3生成中
}
return new SubmitAnswersResponse
{
Success = true,
EstimatedTime = 30 // 预计30秒生成报告
};
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "提交测评答案失败userId: {UserId}, recordId: {RecordId}", userId, request.RecordId);
throw;
}
}
/// <inheritdoc />
public async Task<AssessmentResultDto?> GetResultAsync(long userId, long recordId)
{
_logger.LogDebug("获取测评结果userId: {UserId}, recordId: {RecordId}", userId, recordId);
// 1. 查询测评记录,验证归属和完成状态
var record = await _dbContext.AssessmentRecords
.AsNoTracking()
.Where(r => r.Id == recordId && !r.IsDeleted)
.FirstOrDefaultAsync();
// 记录不存在
if (record == null)
{
_logger.LogWarning("测评记录不存在recordId: {RecordId}", recordId);
return null;
}
// 验证记录归属当前用户Requirements 4.5
if (record.UserId != userId)
{
_logger.LogWarning("测评记录不属于当前用户userId: {UserId}, recordUserId: {RecordUserId}, recordId: {RecordId}",
userId, record.UserId, recordId);
return null;
}
// 验证记录已完成Status=4表示已完成Requirements 4.3
if (record.Status != 4)
{
_logger.LogWarning("测评记录未完成status: {Status}, recordId: {RecordId}", record.Status, recordId);
return null;
}
// 2. 获取测评类型信息
var assessmentType = await _dbContext.AssessmentTypes
.AsNoTracking()
.Where(at => at.Id == record.AssessmentTypeId && !at.IsDeleted)
.FirstOrDefaultAsync();
if (assessmentType == null)
{
_logger.LogWarning("测评类型不存在assessmentTypeId: {AssessmentTypeId}", record.AssessmentTypeId);
return null;
}
// 3. 获取所有报告分类
var categories = await _dbContext.ReportCategories
.AsNoTracking()
.Where(c => c.AssessmentTypeId == record.AssessmentTypeId && !c.IsDeleted)
.OrderBy(c => c.Sort)
.ToListAsync();
// 4. 获取该记录的所有测评结果
var results = await _dbContext.AssessmentResults
.AsNoTracking()
.Where(r => r.RecordId == recordId)
.ToListAsync();
// 5. 组装报告数据
var resultDto = new AssessmentResultDto
{
Id = record.Id,
TypeId = record.AssessmentTypeId,
AssessmentName = assessmentType.Name,
Name = record.Name,
TestDate = record.CompleteTime?.ToString("yyyy-MM-dd") ?? record.CreateTime.ToString("yyyy-MM-dd"),
Intelligences = new List<IntelligenceDto>(),
Traits = new List<TraitDto>(),
Abilities = new List<AbilityDto>(),
InnateLearningTypes = new List<CategoryResultDto>(),
KeyLearningAbilities = new List<CategoryResultDto>(),
BrainTypes = new List<CategoryResultDto>(),
PersonalityTypes = new List<CategoryResultDto>(),
FutureDevelopmentAbilities = new List<CategoryResultDto>(),
Suggestions = new List<string>()
};
// 6. 按分类类型组装数据
foreach (var result in results)
{
var category = categories.FirstOrDefault(c => c.Id == result.CategoryId);
if (category == null) continue;
switch (category.CategoryType)
{
case 1: // 八大智能
resultDto.Intelligences.Add(new IntelligenceDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Level = GetLevelText(result.StarLevel)
});
break;
case 2: // 个人特质
resultDto.Traits.Add(new TraitDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel
});
break;
case 3: // 细分能力
resultDto.Abilities.Add(new AbilityDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Level = GetLevelText(result.StarLevel)
});
break;
case 4: // 先天学习类型
resultDto.InnateLearningTypes.Add(new CategoryResultDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Rank = result.Rank
});
break;
case 5: // 学习关键能力
resultDto.KeyLearningAbilities.Add(new CategoryResultDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Rank = result.Rank
});
break;
case 6: // 科学大脑类型
resultDto.BrainTypes.Add(new CategoryResultDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Rank = result.Rank
});
break;
case 7: // 性格类型
resultDto.PersonalityTypes.Add(new CategoryResultDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Rank = result.Rank
});
break;
case 8: // 未来关键发展能力
resultDto.FutureDevelopmentAbilities.Add(new CategoryResultDto
{
Name = category.Name,
Code = category.Code,
Score = result.Score,
Percentage = result.Percentage,
StarLevel = result.StarLevel,
Rank = result.Rank
});
break;
}
}
// 7. 按得分排序各分类结果
resultDto.Intelligences = resultDto.Intelligences.OrderByDescending(i => i.Score).ToList();
resultDto.Traits = resultDto.Traits.OrderByDescending(t => t.Score).ToList();
resultDto.Abilities = resultDto.Abilities.OrderByDescending(a => a.Score).ToList();
resultDto.InnateLearningTypes = resultDto.InnateLearningTypes.OrderBy(i => i.Rank).ToList();
resultDto.KeyLearningAbilities = resultDto.KeyLearningAbilities.OrderBy(k => k.Rank).ToList();
resultDto.BrainTypes = resultDto.BrainTypes.OrderBy(b => b.Rank).ToList();
resultDto.PersonalityTypes = resultDto.PersonalityTypes.OrderBy(p => p.Rank).ToList();
resultDto.FutureDevelopmentAbilities = resultDto.FutureDevelopmentAbilities.OrderBy(f => f.Rank).ToList();
// 8. 生成综合评价(基于八大智能的最高分和最低分)
if (resultDto.Intelligences.Count > 0)
{
var topIntelligence = resultDto.Intelligences.First();
var bottomIntelligence = resultDto.Intelligences.Last();
resultDto.Summary = $"您的{topIntelligence.Name}表现突出,建议在此领域继续发展。同时可以关注{bottomIntelligence.Name}的提升。";
}
_logger.LogDebug("获取测评结果成功recordId: {RecordId}, intelligenceCount: {IntelligenceCount}, traitCount: {TraitCount}, abilityCount: {AbilityCount}",
recordId, resultDto.Intelligences.Count, resultDto.Traits.Count, resultDto.Abilities.Count);
return resultDto;
}
/// <summary>
/// 根据星级获取等级文本
/// </summary>
/// <param name="starLevel">星级1-5</param>
/// <returns>等级文本</returns>
private static string GetLevelText(int starLevel)
{
return starLevel switch
{
5 => "优秀",
4 => "良好",
3 => "中等",
2 => "一般",
1 => "较弱",
_ => "未知"
};
}
/// <inheritdoc />
public async Task<VerifyInviteCodeResponse> VerifyInviteCodeAsync(string code)
{
_logger.LogDebug("验证邀请码code: {Code}", code);
// 参数验证
if (string.IsNullOrWhiteSpace(code))
{
_logger.LogWarning("邀请码为空");
return new VerifyInviteCodeResponse
{
IsValid = false,
ErrorMessage = "邀请码不能为空"
};
}
// 查询邀请码
var inviteCode = await _dbContext.InviteCodes
.AsNoTracking()
.Where(ic => ic.Code == code.Trim().ToUpper() && !ic.IsDeleted)
.FirstOrDefaultAsync();
// 邀请码不存在
if (inviteCode == null)
{
_logger.LogWarning("邀请码不存在code: {Code}", code);
return new VerifyInviteCodeResponse
{
IsValid = false,
ErrorMessage = "邀请码有误,请重新输入"
};
}
// 邀请码已使用Status=3表示已使用
if (inviteCode.Status == 3)
{
_logger.LogWarning("邀请码已被使用code: {Code}", code);
return new VerifyInviteCodeResponse
{
IsValid = false,
ErrorMessage = "邀请码已被使用"
};
}
// 邀请码未分配Status=1表示未分配不能使用
if (inviteCode.Status == 1)
{
_logger.LogWarning("邀请码未分配code: {Code}", code);
return new VerifyInviteCodeResponse
{
IsValid = false,
ErrorMessage = "邀请码有误,请重新输入"
};
}
// 验证通过Status=2表示已分配可以使用
_logger.LogDebug("邀请码验证通过code: {Code}, id: {Id}", code, inviteCode.Id);
return new VerifyInviteCodeResponse
{
IsValid = true,
InviteCodeId = inviteCode.Id
};
}
/// <summary>
/// 获取状态文本
/// </summary>
/// <param name="status">状态值</param>
/// <returns>状态文本</returns>
private static string GetStatusText(int status)
{
return status switch
{
1 => "待测评",
2 => "测评中",
3 => "生成中",
4 => "已完成",
_ => "未知"
};
}
/// <inheritdoc />
public async Task<List<ScoreOptionDto>> GetScoreOptionsAsync(long typeId)
{
_logger.LogDebug("获取评分标准选项typeId: {TypeId}", typeId);
var options = await _dbContext.ScoreOptions
.AsNoTracking()
.Where(s => s.AssessmentTypeId == typeId && s.Status == 1 && !s.IsDeleted)
.OrderBy(s => s.Sort)
.ThenBy(s => s.Score)
.Select(s => new ScoreOptionDto
{
Score = s.Score,
Label = s.Label,
Description = s.Description
})
.ToListAsync();
return options;
}
/// <inheritdoc />
public async Task<PendingRecordDto?> GetPendingRecordAsync(long userId, long typeId)
{
_logger.LogDebug("查询进行中的测评记录userId: {UserId}, typeId: {TypeId}", userId, typeId);
var record = await _dbContext.AssessmentRecords
.AsNoTracking()
.Where(r => r.UserId == userId
&& r.AssessmentTypeId == typeId
&& (r.Status == 1 || r.Status == 2)
&& !r.IsDeleted)
.OrderByDescending(r => r.CreateTime)
.Select(r => new PendingRecordDto
{
RecordId = r.Id,
OrderId = r.OrderId,
AssessmentTypeId = r.AssessmentTypeId,
Name = r.Name,
Phone = r.Phone,
Gender = r.Gender,
Age = r.Age,
EducationStage = r.EducationStage,
Province = r.Province,
City = r.City,
District = r.District,
Status = r.Status,
CreateTime = r.CreateTime
})
.FirstOrDefaultAsync();
if (record != null)
{
_logger.LogInformation("找到进行中的测评记录userId: {UserId}, recordId: {RecordId}, status: {Status}",
userId, record.RecordId, record.Status);
}
return record;
}
}