This commit is contained in:
zpc 2026-02-24 13:39:51 +08:00
parent 8858e0eef3
commit e43adee35c
7 changed files with 657 additions and 19 deletions

View File

@ -6,36 +6,36 @@
## Tasks
- [ ] 1. 新增实体类并扩展 DbContext
- [ ] 1.1 创建 QuestionCategoryMapping 实体类
- [x] 1. 新增实体类并扩展 DbContext
- [x] 1.1 创建 QuestionCategoryMapping 实体类
- 在 `MiAssessment.Model/Entities/QuestionCategoryMapping.cs` 中创建实体
- 映射到 `question_category_mappings` 表,包含 Id、QuestionId、CategoryId、CreateTime 字段
- 添加 Question 和 ReportCategory 的导航属性及 ForeignKey 特性
- 遵循现有实体类的 XML 注释和命名规范
- _Requirements: 8.3_
- [ ] 1.2 创建 ReportConclusion 实体类
- [x] 1.2 创建 ReportConclusion 实体类
- 在 `MiAssessment.Model/Entities/ReportConclusion.cs` 中创建实体
- 映射到 `report_conclusions` 表,包含 Id、CategoryId、ConclusionType、Title、Content、CreateTime、UpdateTime、IsDeleted 字段
- Content 字段使用 `[Column(TypeName = "nvarchar(max)")]`Title 使用 `[MaxLength(100)]`
- 添加 ReportCategory 的导航属性及 ForeignKey 特性
- _Requirements: 8.3_
- [ ] 1.3 在 MiAssessmentDbContext 中添加 DbSet
- [x] 1.3 在 MiAssessmentDbContext 中添加 DbSet
- 在 `MiAssessment.Model/Data/MiAssessmentDbContext.cs` 的测评业务表区域添加:
- `DbSet<QuestionCategoryMapping> QuestionCategoryMappings`
- `DbSet<ReportConclusion> ReportConclusions`
- _Requirements: 8.1, 8.2_
- [ ] 2. 实现 ReportGenerationService 核心计算逻辑
- [ ] 2.1 创建 ReportGenerationService 类骨架
- [x] 2. 实现 ReportGenerationService 核心计算逻辑
- [x] 2.1 创建 ReportGenerationService 类骨架
- 在 `MiAssessment.Core/Services/ReportGenerationService.cs` 中创建服务类
- 注入 MiAssessmentDbContext 和 ILogger<ReportGenerationService>
- 定义 `GenerateReportAsync(long recordId)` 公开方法
- 实现 ValidateRecord 验证逻辑检查记录存在且未软删除、Status=3、有答案数据
- _Requirements: 9.1, 9.2, 9.3, 9.4_
- [ ] 2.2 实现得分计算逻辑
- [x] 2.2 实现得分计算逻辑
- 在 GenerateReportAsync 中实现 LoadData一次性加载答案、分类映射、叶子分类、结论数据
- 实现 CalculateScores按 ScoreRule 区分累加计分AnswerValue 直接累加和二值计分AnswerValue>=6 计1分否则0分
- 计算每个叶子分类的 Score、MaxScore累加: 题目数×10二值: 题目数×1、PercentageScore/MaxScore×100保留两位小数
@ -57,7 +57,7 @@
- 验证一道题目映射到多个分类时,各分类得分计算独立,互不影响
- **Validates: Requirements 1.3**
- [ ] 2.6 实现排名计算逻辑
- [x] 2.6 实现排名计算逻辑
- 实现 CalculateRanks按 CategoryType 分组,组内按 Percentage 降序排名
- 支持并列排名:相同 Percentage 的分类分配相同排名值
- 仅对叶子节点分类(无子分类)进行排名
@ -73,7 +73,7 @@
- 验证排名结果中仅包含叶子节点分类,父分类不出现在排名结果中
- **Validates: Requirements 2.3**
- [ ] 2.9 实现星级评定逻辑
- [x] 2.9 实现星级评定逻辑
- 实现 AssignStarLevels根据排名位置分配 1-5 星
- N≥2 时公式:`starLevel = 5 - round((rank - 1) / (N - 1) * 4)`
- N=1 时:唯一分类获得 5 星
@ -85,7 +85,7 @@
- 使用 FsCheck 生成随机排名验证排名第1→5星、排名最后→1星、星级在[1,5]范围内、排名靠前星级不低于靠后
- **Validates: Requirements 3.1, 3.2, 3.3**
- [ ] 2.11 实现结论匹配逻辑
- [x] 2.11 实现结论匹配逻辑
- 实现 MatchConclusions星级→结论类型映射5→1, 4→2, 3→2, 2→3, 1→4
- 查询 ReportConclusion 表匹配 CategoryId 和 ConclusionType
- 无匹配结论时记录为空,继续处理其余分类
@ -96,11 +96,11 @@
- 验证所有星级值1-5映射到正确的结论类型5→1, 4→2, 3→2, 2→3, 1→4
- **Validates: Requirements 4.1**
- [ ] 3. Checkpoint - 确保核心计算逻辑正确
- [x] 3. Checkpoint - 确保核心计算逻辑正确
- Ensure all tests pass, ask the user if questions arise.
- [ ] 4. 实现结果持久化与状态流转
- [ ] 4.1 实现 PersistResults 事务逻辑
- [x] 4. 实现结果持久化与状态流转
- [x] 4.1 实现 PersistResults 事务逻辑
- 在 GenerateReportAsync 中实现事务内操作:
- 删除该 RecordId 已有的 AssessmentResult 记录(支持重新生成)
- 批量写入新的 AssessmentResult 记录RecordId、CategoryId、Score、MaxScore、Percentage、Rank、StarLevel、CreateTime
@ -123,20 +123,20 @@
- 验证成功完成后 Status=4CompleteTime 非空且不早于生成开始时间
- **Validates: Requirements 6.1, 6.2**
- [ ] 5. 服务集成与 DI 注册
- [ ] 5.1 在 ServiceModule 中注册 ReportGenerationService
- [x] 5. 服务集成与 DI 注册
- [x] 5.1 在 ServiceModule 中注册 ReportGenerationService
- 在 `MiAssessment.Infrastructure/Modules/ServiceModule.cs` 中添加 ReportGenerationService 的 Autofac 注册
- 注入 MiAssessmentDbContext 和 ILogger<ReportGenerationService>,使用 InstancePerLifetimeScope
- _Requirements: 7.2_
- [ ] 5.2 修改 AssessmentService 集成 ReportGenerationService
- [x] 5.2 修改 AssessmentService 集成 ReportGenerationService
- 在 AssessmentService 构造函数中添加 ReportGenerationService 依赖
- 在 SubmitAnswersAsync 方法的 `transaction.CommitAsync()` 之后添加 `_reportGenerationService.GenerateReportAsync(request.RecordId)` 调用
- 用 try-catch 包裹报告生成调用失败时记录错误日志但不影响答案提交返回状态保持为3
- 更新 ServiceModule 中 AssessmentService 的注册,添加 ReportGenerationService 依赖解析
- _Requirements: 7.1, 7.3_
- [ ] 6. Final checkpoint - 确保所有测试通过
- [x] 6. Final checkpoint - 确保所有测试通过
- Ensure all tests pass, ask the user if questions arise.
## Notes

View File

@ -24,18 +24,22 @@ 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)
ILogger<AssessmentService> logger,
ReportGenerationService reportGenerationService)
{
_dbContext = dbContext;
_logger = logger;
_reportGenerationService = reportGenerationService;
}
/// <inheritdoc />
@ -285,6 +289,17 @@ public class AssessmentService : IAssessmentService
_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,

View File

@ -0,0 +1,503 @@
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 测评报告生成服务
/// </summary>
/// <remarks>
/// 负责根据测评记录ID执行完整的报告生成流水线
/// - 验证测评记录有效性
/// - 计算各分类得分
/// - 排名与星级评定
/// - 匹配结论文本
/// - 持久化结果并更新状态
/// </remarks>
public class ReportGenerationService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<ReportGenerationService> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="logger">日志记录器</param>
public ReportGenerationService(
MiAssessmentDbContext dbContext,
ILogger<ReportGenerationService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <summary>
/// 根据测评记录ID生成报告
/// </summary>
/// <param name="recordId">测评记录ID</param>
public async Task GenerateReportAsync(long recordId)
{
_logger.LogInformation("开始生成测评报告recordId: {RecordId}", recordId);
// 步骤1验证测评记录
var record = await ValidateRecordAsync(recordId);
// 步骤2加载答案、分类映射、叶子分类、结论数据
var (answers, mappings, leafCategories, conclusions) = await LoadDataAsync(recordId);
// 步骤3按 ScoreRule 计算各分类得分
var categoryScores = CalculateScores(answers, mappings, leafCategories);
_logger.LogDebug("得分计算完成recordId: {RecordId}, 分类数量: {Count}", recordId, categoryScores.Count);
// 步骤4按 CategoryType 分组排名
var rankedScores = CalculateRanks(categoryScores);
_logger.LogDebug("排名计算完成recordId: {RecordId}, 排名数量: {Count}", recordId, rankedScores.Count);
// 步骤5根据排名分配星级
var starredScores = AssignStarLevels(rankedScores);
_logger.LogDebug("星级评定完成recordId: {RecordId}, 星级数量: {Count}", recordId, starredScores.Count);
// 步骤6星级→结论类型映射匹配结论文本
var finalResults = MatchConclusions(starredScores, conclusions);
_logger.LogDebug("结论匹配完成recordId: {RecordId}, 结果数量: {Count}", recordId, finalResults.Count);
// 步骤7事务内写入结果并更新状态
await PersistResultsAsync(recordId, finalResults);
_logger.LogInformation("测评报告生成完成recordId: {RecordId}", recordId);
}
/// <summary>
/// 验证测评记录有效性
/// </summary>
/// <param name="recordId">测评记录ID</param>
/// <returns>验证通过的测评记录</returns>
/// <exception cref="InvalidOperationException">记录不存在、已删除、状态不正确或无答案数据时抛出</exception>
private async Task<Model.Entities.AssessmentRecord> ValidateRecordAsync(long recordId)
{
// 检查记录是否存在且未软删除
var record = await _dbContext.AssessmentRecords
.AsNoTracking()
.FirstOrDefaultAsync(r => r.Id == recordId && !r.IsDeleted);
if (record == null)
{
_logger.LogError("测评记录不存在或已删除recordId: {RecordId}", recordId);
throw new InvalidOperationException($"测评记录不存在或已删除recordId: {recordId}");
}
// 检查记录状态是否为3生成中
if (record.Status != 3)
{
_logger.LogError("测评记录状态不正确recordId: {RecordId}, 当前状态: {Status}, 期望状态: 3", recordId, record.Status);
throw new InvalidOperationException($"测评记录状态不正确recordId: {recordId}, 当前状态: {record.Status}, 期望状态: 3");
}
// 检查是否有答案数据
var answerCount = await _dbContext.AssessmentAnswers
.CountAsync(a => a.RecordId == recordId);
if (answerCount == 0)
{
_logger.LogError("测评记录无答案数据recordId: {RecordId}", recordId);
throw new InvalidOperationException($"测评记录无答案数据recordId: {recordId}");
}
_logger.LogDebug("测评记录验证通过recordId: {RecordId}, 答案数量: {AnswerCount}", recordId, answerCount);
return record;
}
/// <summary>
/// 一次性加载报告生成所需的全部数据
/// </summary>
/// <param name="recordId">测评记录ID</param>
/// <returns>答案列表、分类映射列表、叶子分类列表、结论列表</returns>
private async Task<(
List<AssessmentAnswer> answers,
List<QuestionCategoryMapping> mappings,
List<ReportCategory> leafCategories,
List<ReportConclusion> conclusions)> LoadDataAsync(long recordId)
{
// 加载该记录的所有答案
var answers = await _dbContext.AssessmentAnswers
.AsNoTracking()
.Where(a => a.RecordId == recordId)
.ToListAsync();
// 加载所有题目-分类映射
var mappings = await _dbContext.QuestionCategoryMappings
.AsNoTracking()
.ToListAsync();
// 加载所有分类,用于确定叶子节点
var allCategories = await _dbContext.ReportCategories
.AsNoTracking()
.Where(c => !c.IsDeleted)
.ToListAsync();
// 叶子分类:其 Id 不被任何其他分类的 ParentId 引用
var parentIds = new HashSet<long>(allCategories.Select(c => c.ParentId));
var leafCategories = allCategories
.Where(c => !parentIds.Contains(c.Id))
.ToList();
// 加载所有未删除的结论数据
var conclusions = await _dbContext.ReportConclusions
.AsNoTracking()
.Where(c => !c.IsDeleted)
.ToListAsync();
_logger.LogDebug("数据加载完成,答案: {AnswerCount}, 映射: {MappingCount}, 叶子分类: {LeafCount}, 结论: {ConclusionCount}",
answers.Count, mappings.Count, leafCategories.Count, conclusions.Count);
return (answers, mappings, leafCategories, conclusions);
}
/// <summary>
/// 按 ScoreRule 计算各叶子分类的得分、满分和百分比
/// </summary>
/// <param name="answers">答案列表</param>
/// <param name="mappings">题目-分类映射列表</param>
/// <param name="leafCategories">叶子分类列表</param>
/// <returns>各分类的得分计算结果</returns>
internal static List<CategoryScore> CalculateScores(
List<AssessmentAnswer> answers,
List<QuestionCategoryMapping> mappings,
List<ReportCategory> leafCategories)
{
// 构建题目ID → 答案值的快速查找字典
var answerMap = answers.ToDictionary(a => a.QuestionId, a => a.AnswerValue);
// 构建分类ID → 该分类下的题目ID列表通过映射表
var categoryQuestionMap = mappings
.GroupBy(m => m.CategoryId)
.ToDictionary(g => g.Key, g => g.Select(m => m.QuestionId).ToList());
var leafCategoryIds = new HashSet<long>(leafCategories.Select(c => c.Id));
var scores = new List<CategoryScore>();
foreach (var category in leafCategories)
{
// 获取该分类下的题目ID列表
if (!categoryQuestionMap.TryGetValue(category.Id, out var questionIds))
{
// 该分类无映射题目,跳过
continue;
}
// 筛选出有答案的题目值
var answerValues = questionIds
.Where(qId => answerMap.ContainsKey(qId))
.Select(qId => answerMap[qId])
.ToList();
if (answerValues.Count == 0)
{
// 该分类下无有效答案,跳过
continue;
}
decimal score;
decimal maxScore;
if (category.ScoreRule == 1)
{
// 累加计分AnswerValue 直接累加,满分 = 题目数 × 10
score = answerValues.Sum();
maxScore = answerValues.Count * 10m;
}
else
{
// 二值计分AnswerValue >= 6 计1分否则0分满分 = 题目数 × 1
score = answerValues.Count(v => v >= 6);
maxScore = answerValues.Count * 1m;
}
// 百分比 = Score / MaxScore × 100保留两位小数
var percentage = Math.Round(score / maxScore * 100m, 2);
scores.Add(new CategoryScore(category.Id, category.CategoryType, score, maxScore, percentage));
}
return scores;
}
/// <summary>
/// 分类得分计算结果
/// </summary>
/// <param name="CategoryId">分类ID</param>
/// <param name="CategoryType">分类类型1-8</param>
/// <param name="Score">得分</param>
/// <param name="MaxScore">满分</param>
/// <param name="Percentage">百分比</param>
internal record CategoryScore(long CategoryId, int CategoryType, decimal Score, decimal MaxScore, decimal Percentage);
/// <summary>
/// 按 CategoryType 分组,组内按 Percentage 降序排名
/// </summary>
/// <remarks>
/// 排名规则:
/// - 按 CategoryType 分组,每组独立排名
/// - 组内按 Percentage 降序排列,最高百分比排名为 1
/// - 相同 Percentage 的分类分配相同排名值(并列排名)
/// - 并列排名后下一个排名跳过并列数量如两个并列第1下一个为第3
/// </remarks>
/// <param name="scores">各分类的得分计算结果</param>
/// <returns>带排名信息的分类得分列表</returns>
internal static List<RankedCategoryScore> CalculateRanks(List<CategoryScore> scores)
{
var result = new List<RankedCategoryScore>();
// 按 CategoryType 分组,每组独立排名
var groups = scores.GroupBy(s => s.CategoryType);
foreach (var group in groups)
{
// 组内按 Percentage 降序排列
var sorted = group.OrderByDescending(s => s.Percentage).ToList();
var rank = 1;
for (var i = 0; i < sorted.Count; i++)
{
// 如果不是第一个元素,且百分比与前一个不同,则排名 = 当前位置 + 1
if (i > 0 && sorted[i].Percentage != sorted[i - 1].Percentage)
{
rank = i + 1;
}
var s = sorted[i];
result.Add(new RankedCategoryScore(s.CategoryId, s.CategoryType, s.Score, s.MaxScore, s.Percentage, rank));
}
}
return result;
}
/// <summary>
/// 带排名的分类得分计算结果
/// </summary>
/// <param name="CategoryId">分类ID</param>
/// <param name="CategoryType">分类类型1-8</param>
/// <param name="Score">得分</param>
/// <param name="MaxScore">满分</param>
/// <param name="Percentage">百分比</param>
/// <param name="Rank">组内排名1为最高</param>
internal record RankedCategoryScore(long CategoryId, int CategoryType, decimal Score, decimal MaxScore, decimal Percentage, int Rank);
/// <summary>
/// 根据排名位置分配 1-5 星级
/// </summary>
/// <remarks>
/// 星级分配规则:
/// - 按 CategoryType 分组,每组独立计算星级
/// - N=1 时:唯一分类获得 5 星
/// - N≥2 时starLevel = 5 - round((rank - 1) / (N - 1) * 4),结果限制在 [1, 5] 范围内
/// - 相同排名的分类自然获得相同星级(公式保证)
/// </remarks>
/// <param name="rankedScores">带排名的分类得分列表</param>
/// <returns>带星级的分类得分列表</returns>
internal static List<StarredCategoryScore> AssignStarLevels(List<RankedCategoryScore> rankedScores)
{
var result = new List<StarredCategoryScore>();
// 按 CategoryType 分组,每组独立计算星级
var groups = rankedScores.GroupBy(s => s.CategoryType);
foreach (var group in groups)
{
var items = group.ToList();
var n = items.Count;
foreach (var item in items)
{
int starLevel;
if (n == 1)
{
// 唯一分类获得 5 星
starLevel = 5;
}
else
{
// 公式starLevel = 5 - round((rank - 1) / (N - 1) * 4)
starLevel = 5 - (int)Math.Round((item.Rank - 1.0) / (n - 1.0) * 4.0);
// 限制在 [1, 5] 范围内
starLevel = Math.Clamp(starLevel, 1, 5);
}
result.Add(new StarredCategoryScore(
item.CategoryId, item.CategoryType,
item.Score, item.MaxScore, item.Percentage,
item.Rank, starLevel));
}
}
return result;
}
/// <summary>
/// 带星级的分类得分计算结果
/// </summary>
/// <param name="CategoryId">分类ID</param>
/// <param name="CategoryType">分类类型1-8</param>
/// <param name="Score">得分</param>
/// <param name="MaxScore">满分</param>
/// <param name="Percentage">百分比</param>
/// <param name="Rank">组内排名1为最高</param>
/// <param name="StarLevel">星级1-5</param>
internal record StarredCategoryScore(long CategoryId, int CategoryType, decimal Score, decimal MaxScore, decimal Percentage, int Rank, int StarLevel);
/// <summary>
/// 最终分类结果(含结论匹配)
/// </summary>
/// <param name="CategoryId">分类ID</param>
/// <param name="CategoryType">分类类型1-8</param>
/// <param name="Score">得分</param>
/// <param name="MaxScore">满分</param>
/// <param name="Percentage">百分比</param>
/// <param name="Rank">组内排名1为最高</param>
/// <param name="StarLevel">星级1-5</param>
/// <param name="ConclusionType">结论类型1最强 2较强 3较弱 4最弱</param>
/// <param name="ConclusionContent">结论内容,无匹配时为 null</param>
internal record FinalCategoryResult(long CategoryId, int CategoryType, decimal Score, decimal MaxScore, decimal Percentage, int Rank, int StarLevel, int ConclusionType, string? ConclusionContent);
/// <summary>
/// 将星级映射为结论类型
/// </summary>
/// <remarks>
/// 映射规则:
/// - 5星 → 结论类型1最强
/// - 4星 → 结论类型2较强
/// - 3星 → 结论类型2较强
/// - 2星 → 结论类型3较弱
/// - 1星 → 结论类型4最弱
/// </remarks>
/// <param name="starLevel">星级1-5</param>
/// <returns>结论类型1-4</returns>
internal static int MapStarToConclusionType(int starLevel)
{
return starLevel switch
{
5 => 1, // 最强
4 => 2, // 较强
3 => 2, // 较强
2 => 3, // 较弱
1 => 4, // 最弱
_ => throw new ArgumentOutOfRangeException(nameof(starLevel), starLevel, "星级必须在 1-5 范围内")
};
}
/// <summary>
/// 根据星级匹配结论文本
/// </summary>
/// <remarks>
/// 匹配逻辑:
/// - 将星级映射为结论类型(通过 MapStarToConclusionType
/// - 在结论列表中查找匹配 CategoryId 和 ConclusionType 的记录
/// - 找到则使用其 Content 字段,未找到则结论内容为 null继续处理其余分类
/// </remarks>
/// <param name="starredScores">带星级的分类得分列表</param>
/// <param name="conclusions">报告结论列表</param>
/// <returns>包含结论匹配结果的最终分类结果列表</returns>
internal static List<FinalCategoryResult> MatchConclusions(
List<StarredCategoryScore> starredScores,
List<ReportConclusion> conclusions)
{
// 构建 (CategoryId, ConclusionType) → Content 的快速查找字典
var conclusionMap = conclusions
.GroupBy(c => (c.CategoryId, c.ConclusionType))
.ToDictionary(g => g.Key, g => g.First().Content);
var results = new List<FinalCategoryResult>();
foreach (var item in starredScores)
{
// 星级→结论类型映射
var conclusionType = MapStarToConclusionType(item.StarLevel);
// 查找匹配的结论内容
conclusionMap.TryGetValue((item.CategoryId, conclusionType), out var conclusionContent);
results.Add(new FinalCategoryResult(
item.CategoryId, item.CategoryType,
item.Score, item.MaxScore, item.Percentage,
item.Rank, item.StarLevel,
conclusionType, conclusionContent));
}
return results;
}
/// <summary>
/// 在事务内持久化计算结果并更新测评记录状态
/// </summary>
/// <remarks>
/// 事务内执行以下操作:
/// - 删除该 RecordId 已有的 AssessmentResult 记录(支持重新生成)
/// - 批量写入新的 AssessmentResult 记录
/// - 更新 AssessmentRecord 的 Status 为 4已完成并设置 CompleteTime
/// 异常时回滚事务Status 保持为 3生成中
/// </remarks>
/// <param name="recordId">测评记录ID</param>
/// <param name="finalResults">最终分类计算结果列表</param>
private async Task PersistResultsAsync(long recordId, List<FinalCategoryResult> finalResults)
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 删除该 RecordId 已有的结果记录(支持重新生成)
var existingResults = await _dbContext.AssessmentResults
.Where(r => r.RecordId == recordId)
.ToListAsync();
if (existingResults.Count > 0)
{
_dbContext.AssessmentResults.RemoveRange(existingResults);
_logger.LogDebug("删除已有结果记录recordId: {RecordId}, 数量: {Count}", recordId, existingResults.Count);
}
// 批量写入新的 AssessmentResult 记录
var now = DateTime.Now;
var newResults = finalResults.Select(r => new AssessmentResult
{
RecordId = recordId,
CategoryId = r.CategoryId,
Score = r.Score,
MaxScore = r.MaxScore,
Percentage = r.Percentage,
Rank = r.Rank,
StarLevel = r.StarLevel,
CreateTime = now
}).ToList();
await _dbContext.AssessmentResults.AddRangeAsync(newResults);
// 重新加载测评记录带跟踪更新状态为4已完成
var record = await _dbContext.AssessmentRecords
.FirstAsync(r => r.Id == recordId);
record.Status = 4;
record.CompleteTime = now;
record.UpdateTime = now;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("结果持久化完成recordId: {RecordId}, 写入记录数: {Count}", recordId, newResults.Count);
}
catch (Exception ex)
{
// 回滚事务Status 保持为 3生成中
await transaction.RollbackAsync();
_logger.LogError(ex, "结果持久化失败事务已回滚recordId: {RecordId}", recordId);
throw;
}
}
}

View File

@ -131,12 +131,21 @@ public class ServiceModule : Module
// ========== 小程序测评模块服务注册 ==========
// 注册报告生成服务
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var logger = c.Resolve<ILogger<ReportGenerationService>>();
return new ReportGenerationService(dbContext, logger);
}).InstancePerLifetimeScope();
// 注册测评服务
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var logger = c.Resolve<ILogger<AssessmentService>>();
return new AssessmentService(dbContext, logger);
var reportGenerationService = c.Resolve<ReportGenerationService>();
return new AssessmentService(dbContext, logger, reportGenerationService);
}).As<IAssessmentService>().InstancePerLifetimeScope();
// ========== 小程序订单模块服务注册 ==========

View File

@ -57,6 +57,10 @@ public partial class MiAssessmentDbContext : DbContext
public virtual DbSet<ReportCategory> ReportCategories { get; set; }
public virtual DbSet<QuestionCategoryMapping> QuestionCategoryMappings { get; set; }
public virtual DbSet<ReportConclusion> ReportConclusions { get; set; }
public virtual DbSet<ScoreOption> ScoreOptions { get; set; }
public virtual DbSet<Order> Orders { get; set; }

View File

@ -0,0 +1,45 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 题目分类映射表
/// </summary>
[Table("question_category_mappings")]
public class QuestionCategoryMapping
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public long Id { get; set; }
/// <summary>
/// 题目ID
/// </summary>
public long QuestionId { get; set; }
/// <summary>
/// 分类ID
/// </summary>
public long CategoryId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 关联的题目
/// </summary>
[ForeignKey(nameof(QuestionId))]
public virtual Question? Question { get; set; }
/// <summary>
/// 关联的报告分类
/// </summary>
[ForeignKey(nameof(CategoryId))]
public virtual ReportCategory? Category { get; set; }
}

View File

@ -0,0 +1,62 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 报告结论表
/// </summary>
[Table("report_conclusions")]
public class ReportConclusion
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public long Id { get; set; }
/// <summary>
/// 分类ID
/// </summary>
public long CategoryId { get; set; }
/// <summary>
/// 结论类型1最强 2较强 3较弱 4最弱
/// </summary>
public int ConclusionType { get; set; }
/// <summary>
/// 结论标题
/// </summary>
[MaxLength(100)]
public string? Title { get; set; }
/// <summary>
/// 结论内容
/// </summary>
[Required]
[Column(TypeName = "nvarchar(max)")]
public string Content { get; set; } = null!;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdateTime { get; set; }
/// <summary>
/// 软删除标记
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// 关联的报告分类
/// </summary>
[ForeignKey(nameof(CategoryId))]
public virtual ReportCategory? Category { get; set; }
}