This commit is contained in:
zpc 2026-02-24 13:08:13 +08:00
parent ab9d7dd262
commit 8858e0eef3
2 changed files with 511 additions and 0 deletions

View File

@ -0,0 +1,362 @@
# 设计文档:测评结论生成服务
## 概述
测评结论生成服务ReportGenerationService是学业邑规划测评系统中衔接"答案提交"与"报告查看"的核心后端模块。用户完成80道测评题目提交答案后`AssessmentService.SubmitAnswersAsync` 将测评记录状态设为3生成中并提交事务随后调用本服务执行以下流水线
1. **得分计算**:根据 ScoreRule累加/二值)计算各叶子分类的得分、满分和百分比
2. **排名计算**:在同一 CategoryType 维度内按百分比降序排名(支持并列)
3. **星级评定**根据排名位置分配1-5星
4. **结论匹配**:将星级映射为结论类型,查询对应结论文本
5. **结果持久化**:在单次事务中写入所有 AssessmentResult 记录并更新状态为4已完成
该服务作为独立类注入 DI 容器,使用 `MiAssessmentDbContext` 访问 Business 库。
### 设计决策
- **同步调用而非异步队列**当前测评数据量80题 × 8维度计算量小同步调用即可满足性能要求避免引入消息队列的复杂度
- **在 MiAssessment.Core 项目中实现**:与 AssessmentService 同层,便于直接依赖注入和调用
- **复用 MiAssessmentDbContext**:遵循双数据库架构规范,小程序 API 通过此上下文读写 Business 库
- **需在 MiAssessment.Model 中新增实体**QuestionCategoryMapping 和 ReportConclusion 实体目前仅存在于 Admin.Business 项目中,需在 Model 项目中创建对应实体以供 Core 层使用
## 架构
### 调用流程
```mermaid
sequenceDiagram
participant Client as 小程序客户端
participant API as AssessmentController
participant AS as AssessmentService
participant RGS as ReportGenerationService
participant DB as MiAssessmentDbContext
Client->>API: POST /api/assessment/submitAnswers
API->>AS: SubmitAnswersAsync(userId, request)
AS->>DB: 保存答案 + 状态设为3生成中
AS->>DB: CommitAsync()
AS->>RGS: GenerateReportAsync(recordId)
RGS->>DB: 查询答案、分类映射、结论
RGS->>RGS: 计算得分 → 排名 → 星级 → 匹配结论
RGS->>DB: BeginTransaction
RGS->>DB: 删除旧结果 + 写入新结果 + 状态设为4
RGS->>DB: CommitAsync()
RGS-->>AS: 完成
AS-->>API: SubmitAnswersResponse
API-->>Client: { success: true }
```
### 项目层级
```
MiAssessment.Model/
├── Entities/
│ ├── QuestionCategoryMapping.cs ← 新增
│ └── ReportConclusion.cs ← 新增
└── Data/
└── MiAssessmentDbContext.cs ← 新增 DbSet
MiAssessment.Core/
└── Services/
├── AssessmentService.cs ← 修改:调用 ReportGenerationService
└── ReportGenerationService.cs ← 新增
```
## 组件与接口
### ReportGenerationService
```csharp
namespace MiAssessment.Core.Services;
/// <summary>
/// 测评报告生成服务
/// </summary>
public class ReportGenerationService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<ReportGenerationService> _logger;
public ReportGenerationService(
MiAssessmentDbContext dbContext,
ILogger<ReportGenerationService> logger);
/// <summary>
/// 根据测评记录ID生成报告
/// </summary>
/// <param name="recordId">测评记录ID</param>
public async Task GenerateReportAsync(long recordId);
}
```
### 内部处理步骤
`GenerateReportAsync` 方法内部按以下顺序执行:
1. **ValidateRecord(recordId)**验证记录存在、未软删除、Status=3、有答案数据
2. **LoadData(record)**:一次性加载答案、分类映射、叶子分类、结论数据
3. **CalculateScores(answers, mappings, categories)**:按 ScoreRule 计算各分类得分
4. **CalculateRanks(scores)**:按 CategoryType 分组排名
5. **AssignStarLevels(rankedScores)**:根据排名分配星级
6. **MatchConclusions(starResults, conclusions)**:星级→结论类型映射
7. **PersistResults(recordId, results)**:事务内写入结果并更新状态
### AssessmentService 集成修改
`SubmitAnswersAsync` 方法的事务提交成功后,添加对 `ReportGenerationService.GenerateReportAsync` 的调用:
```csharp
// 现有代码:事务提交后
await transaction.CommitAsync();
// 新增:触发报告生成
try
{
await _reportGenerationService.GenerateReportAsync(request.RecordId);
}
catch (Exception ex)
{
_logger.LogError(ex, "报告生成失败recordId: {RecordId}", request.RecordId);
// 报告生成失败不影响答案提交的返回状态保持为3生成中
}
```
## 数据模型
### 新增实体QuestionCategoryMappingMiAssessment.Model
参照 `MiAssessment.Admin.Business.Entities.QuestionCategoryMapping` 的结构,在 Model 项目中创建对应实体:
```csharp
namespace MiAssessment.Model.Entities;
[Table("question_category_mappings")]
public class QuestionCategoryMapping
{
[Key]
public long Id { get; set; }
public long QuestionId { get; set; }
public long CategoryId { get; set; }
public DateTime CreateTime { get; set; }
[ForeignKey(nameof(QuestionId))]
public virtual Question? Question { get; set; }
[ForeignKey(nameof(CategoryId))]
public virtual ReportCategory? Category { get; set; }
}
```
### 新增实体ReportConclusionMiAssessment.Model
参照 `MiAssessment.Admin.Business.Entities.ReportConclusion` 的结构:
```csharp
namespace MiAssessment.Model.Entities;
[Table("report_conclusions")]
public class ReportConclusion
{
[Key]
public long Id { get; set; }
public long CategoryId { get; set; }
public int ConclusionType { get; set; }
[MaxLength(100)]
public string? Title { get; set; }
[Required]
[Column(TypeName = "nvarchar(max)")]
public string Content { get; set; } = null!;
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
public bool IsDeleted { get; set; }
[ForeignKey(nameof(CategoryId))]
public virtual ReportCategory? Category { get; set; }
}
```
### MiAssessmentDbContext 扩展
新增两个 DbSet
```csharp
public virtual DbSet<QuestionCategoryMapping> QuestionCategoryMappings { get; set; }
public virtual DbSet<ReportConclusion> ReportConclusions { get; set; }
```
### 核心计算逻辑数据流
```mermaid
flowchart TD
A[AssessmentAnswer 列表] --> B[按 QuestionCategoryMapping 分组到各 Category]
B --> C{ScoreRule?}
C -->|1 累加| D[Score = Σ AnswerValue]
C -->|2 二值| E[Score = Σ (AnswerValue >= 6 ? 1 : 0)]
D --> F[MaxScore = 题目数 × 10]
E --> G[MaxScore = 题目数 × 1]
F --> H[Percentage = Score / MaxScore × 100]
G --> H
H --> I[按 CategoryType 分组]
I --> J[组内按 Percentage 降序排名]
J --> K[根据排名位置分配 1-5 星]
K --> L{StarLevel 映射}
L -->|5星| M[ConclusionType = 1 最强]
L -->|4星| N[ConclusionType = 2 较强]
L -->|3星| N
L -->|2星| O[ConclusionType = 3 较弱]
L -->|1星| P[ConclusionType = 4 最弱]
M --> Q[查询 ReportConclusion]
N --> Q
O --> Q
P --> Q
Q --> R[写入 AssessmentResult]
```
### 星级分配算法
对于同一 CategoryType 维度内的 N 个叶子分类(已按百分比降序排名):
- **N ≥ 5**排名第1 → 5星排名最后 → 1星中间按排名位置均匀映射到 2-4 星
- 公式:`starLevel = 5 - round((rank - 1) / (N - 1) * 4)`
- **N < 5**排名第1 → 5星排名最后 → 1星中间按比例分配
- 公式同上,适用于任意 N ≥ 2
- **N = 1**:唯一分类 → 5星
- **并列排名**:相同百分比的分类获得相同排名和相同星级
## 正确性属性Correctness Properties
*属性Property是指在系统所有合法执行中都应成立的特征或行为——本质上是对系统应做之事的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: 累加计分正确性
*For any* 一组答案AnswerValue 范围 1-10和一个 ScoreRule=1 的叶子分类该分类的计算结果应满足Score 等于所有映射题目 AnswerValue 之和MaxScore 等于映射题目数量乘以 10Percentage 等于 round(Score / MaxScore × 100, 2)。
**Validates: Requirements 1.1, 1.4, 1.5**
### Property 2: 二值计分正确性
*For any* 一组答案AnswerValue 范围 1-10和一个 ScoreRule=2 的叶子分类该分类的计算结果应满足Score 等于映射题目中 AnswerValue ≥ 6 的数量MaxScore 等于映射题目数量乘以 1Percentage 等于 round(Score / MaxScore × 100, 2)。
**Validates: Requirements 1.2, 1.4, 1.5**
### Property 3: 多分类映射得分独立性
*For any* 一道题目映射到多个分类的情况,每个分类的得分计算应独立包含该题目的贡献,且一道题目对不同分类的贡献互不影响。
**Validates: Requirements 1.3**
### Property 4: 排名顺序正确性
*For any* 同一 CategoryType 维度内的一组叶子分类及其百分比,排名结果应满足:百分比更高的分类排名值更小(更靠前),且百分比相同的分类排名值相同。
**Validates: Requirements 2.1, 2.2**
### Property 5: 仅叶子节点参与排名
*For any* 包含父子层级关系的分类树排名结果中应仅包含叶子节点分类ParentId 不被其他分类引用的分类),父分类不应出现在排名结果中。
**Validates: Requirements 2.3**
### Property 6: 星级分配正确性
*For any* 同一 CategoryType 维度内 N 个已排名的叶子分类N ≥ 2星级分配应满足排名第 1 的分类获得 5 星,排名最后的分类获得 1 星,所有星级值在 [1, 5] 范围内,且排名更靠前的分类星级不低于排名更靠后的分类。当 N = 1 时,唯一分类获得 5 星。
**Validates: Requirements 3.1, 3.2, 3.3**
### Property 7: 星级到结论类型映射正确性
*For any* 星级值1-5映射到的结论类型应满足5→1最强4→2较强3→2较强2→3较弱1→4最弱
**Validates: Requirements 4.1**
### Property 8: 结果持久化完整性
*For any* 成功完成的报告生成,写入 AssessmentResult 表的记录应与计算结果一一对应:每条记录的 RecordId、CategoryId、Score、MaxScore、Percentage、Rank、StarLevel 均与内存计算值一致。
**Validates: Requirements 5.1, 4.2**
### Property 9: 重新生成幂等性
*For any* 已有 AssessmentResult 记录的测评记录,重新执行报告生成后,数据库中该 RecordId 的结果记录应完全等于最新一次计算的结果,不存在旧数据残留。
**Validates: Requirements 5.3**
### Property 10: 生成完成后状态与时间一致性
*For any* 成功完成报告生成的测评记录,其 Status 应为 4已完成CompleteTime 应非空且不早于生成开始时间。
**Validates: Requirements 6.1, 6.2**
## 错误处理
| 场景 | 处理方式 | 状态影响 |
|------|----------|----------|
| RecordId 对应的记录不存在或已软删除 | 抛出 `InvalidOperationException`,记录错误日志 | 无状态变更 |
| Record.Status ≠ 3 | 抛出 `InvalidOperationException`,记录错误日志 | 无状态变更 |
| 答案记录数量为 0 | 抛出 `InvalidOperationException`,记录错误日志 | 无状态变更 |
| 数据库写入异常 | 回滚事务,记录错误日志 | Status 保持为 3生成中 |
| 某分类无匹配结论 | 结论文本记录为空,继续处理其余分类 | 不影响整体流程 |
错误处理遵循以下原则:
- 验证阶段的错误(记录不存在、状态不对、无答案)直接抛出异常,由调用方捕获
- 持久化阶段的错误通过事务回滚保证数据一致性
- 结论缺失属于数据不完整而非系统错误,采用容错处理
## 测试策略
### 测试框架
- 单元测试xUnit + Moq
- 属性测试FsCheck.NET 属性测试库)
- 数据库测试:使用 EF Core InMemory Provider 或 SQLite InMemory
### 单元测试
单元测试覆盖以下场景:
1. **得分计算**:验证累加计分和二值计分的具体示例
2. **排名计算**:验证正常排名、并列排名的具体示例
3. **星级分配**:验证 N=1, N=3, N=5, N=8 等不同分类数量的星级分配
4. **结论匹配**:验证有结论和无结论的情况
5. **错误处理**:验证各种异常输入(记录不存在、状态不对、无答案等)
6. **集成测试**:验证完整的报告生成流程(从答案到最终结果)
### 属性测试
每个正确性属性对应一个属性测试,使用 FsCheck 生成随机输入,最少运行 100 次迭代。
属性测试标注格式:
```
Feature: assessment-report-generation, Property {number}: {property_text}
```
示例:
```csharp
/// <summary>
/// Feature: assessment-report-generation, Property 1: 累加计分正确性
/// *For any* 一组答案和 ScoreRule=1 的分类Score 应等于 AnswerValue 之和
/// **Validates: Requirements 1.1, 1.4, 1.5**
/// </summary>
[Property(MaxTest = 100)]
public Property CumulativeScoring_ShouldSumAnswerValues()
{
// FsCheck 生成随机答案值列表,验证计算结果
}
```
### 测试覆盖矩阵
| 属性 | 属性测试 | 单元测试 | 边界情况 |
|------|----------|----------|----------|
| Property 1: 累加计分 | ✓ 随机答案值 | ✓ 具体示例 | 全1分、全10分 |
| Property 2: 二值计分 | ✓ 随机答案值 | ✓ 具体示例 | 全5分边界、全6分 |
| Property 3: 多分类映射 | ✓ 随机映射关系 | ✓ 一题多分类示例 | - |
| Property 4: 排名顺序 | ✓ 随机百分比集合 | ✓ 并列排名示例 | 全部相同百分比 |
| Property 5: 叶子节点 | ✓ 随机分类树 | ✓ 含父子层级示例 | - |
| Property 6: 星级分配 | ✓ 随机排名 | ✓ N=1,3,5,8 示例 | N=1, N=2 |
| Property 7: 星级映射 | ✓ 所有星级值 | - | - |
| Property 8: 持久化完整性 | ✓ 随机计算结果 | ✓ 完整流程示例 | - |
| Property 9: 重新生成 | ✓ 随机旧数据+新数据 | ✓ 覆盖示例 | - |
| Property 10: 状态一致性 | ✓ 随机成功场景 | ✓ 状态验证示例 | - |

View File

@ -0,0 +1,149 @@
# Implementation Plan: 测评结论生成服务
## Overview
将设计文档中的报告生成流水线转化为可执行的编码任务。按照数据模型 → 核心计算逻辑 → 服务集成 → DI注册的顺序递增实现确保每一步都可编译运行。所有代码在 MiAssessment.Model 和 MiAssessment.Core 项目中实现,使用 MiAssessmentDbContext 访问 Business 库。
## Tasks
- [ ] 1. 新增实体类并扩展 DbContext
- [ ] 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 实体类
- 在 `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
- 在 `MiAssessment.Model/Data/MiAssessmentDbContext.cs` 的测评业务表区域添加:
- `DbSet<QuestionCategoryMapping> QuestionCategoryMappings`
- `DbSet<ReportConclusion> ReportConclusions`
- _Requirements: 8.1, 8.2_
- [ ] 2. 实现 ReportGenerationService 核心计算逻辑
- [ ] 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 实现得分计算逻辑
- 在 GenerateReportAsync 中实现 LoadData一次性加载答案、分类映射、叶子分类、结论数据
- 实现 CalculateScores按 ScoreRule 区分累加计分AnswerValue 直接累加和二值计分AnswerValue>=6 计1分否则0分
- 计算每个叶子分类的 Score、MaxScore累加: 题目数×10二值: 题目数×1、PercentageScore/MaxScore×100保留两位小数
- 通过 QuestionCategoryMapping 确定题目所属分类,支持一题多分类
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [ ]* 2.3 编写属性测试:累加计分正确性
- **Property 1: 累加计分正确性**
- 使用 FsCheck 生成随机 AnswerValue1-10列表验证 ScoreRule=1 时 Score = Σ AnswerValueMaxScore = N×10Percentage = round(Score/MaxScore×100, 2)
- **Validates: Requirements 1.1, 1.4, 1.5**
- [ ]* 2.4 编写属性测试:二值计分正确性
- **Property 2: 二值计分正确性**
- 使用 FsCheck 生成随机 AnswerValue1-10列表验证 ScoreRule=2 时 Score = count(AnswerValue>=6)MaxScore = N×1
- **Validates: Requirements 1.2, 1.4, 1.5**
- [ ]* 2.5 编写属性测试:多分类映射得分独立性
- **Property 3: 多分类映射得分独立性**
- 验证一道题目映射到多个分类时,各分类得分计算独立,互不影响
- **Validates: Requirements 1.3**
- [ ] 2.6 实现排名计算逻辑
- 实现 CalculateRanks按 CategoryType 分组,组内按 Percentage 降序排名
- 支持并列排名:相同 Percentage 的分类分配相同排名值
- 仅对叶子节点分类(无子分类)进行排名
- _Requirements: 2.1, 2.2, 2.3_
- [ ]* 2.7 编写属性测试:排名顺序正确性
- **Property 4: 排名顺序正确性**
- 使用 FsCheck 生成随机百分比集合,验证百分比更高的分类排名更靠前,相同百分比排名相同
- **Validates: Requirements 2.1, 2.2**
- [ ]* 2.8 编写属性测试:仅叶子节点参与排名
- **Property 5: 仅叶子节点参与排名**
- 验证排名结果中仅包含叶子节点分类,父分类不出现在排名结果中
- **Validates: Requirements 2.3**
- [ ] 2.9 实现星级评定逻辑
- 实现 AssignStarLevels根据排名位置分配 1-5 星
- N≥2 时公式:`starLevel = 5 - round((rank - 1) / (N - 1) * 4)`
- N=1 时:唯一分类获得 5 星
- 并列排名的分类获得相同星级
- _Requirements: 3.1, 3.2, 3.3_
- [ ]* 2.10 编写属性测试:星级分配正确性
- **Property 6: 星级分配正确性**
- 使用 FsCheck 生成随机排名验证排名第1→5星、排名最后→1星、星级在[1,5]范围内、排名靠前星级不低于靠后
- **Validates: Requirements 3.1, 3.2, 3.3**
- [ ] 2.11 实现结论匹配逻辑
- 实现 MatchConclusions星级→结论类型映射5→1, 4→2, 3→2, 2→3, 1→4
- 查询 ReportConclusion 表匹配 CategoryId 和 ConclusionType
- 无匹配结论时记录为空,继续处理其余分类
- _Requirements: 4.1, 4.2, 4.3_
- [ ]* 2.12 编写属性测试:星级到结论类型映射正确性
- **Property 7: 星级到结论类型映射正确性**
- 验证所有星级值1-5映射到正确的结论类型5→1, 4→2, 3→2, 2→3, 1→4
- **Validates: Requirements 4.1**
- [ ] 3. Checkpoint - 确保核心计算逻辑正确
- Ensure all tests pass, ask the user if questions arise.
- [ ] 4. 实现结果持久化与状态流转
- [ ] 4.1 实现 PersistResults 事务逻辑
- 在 GenerateReportAsync 中实现事务内操作:
- 删除该 RecordId 已有的 AssessmentResult 记录(支持重新生成)
- 批量写入新的 AssessmentResult 记录RecordId、CategoryId、Score、MaxScore、Percentage、Rank、StarLevel、CreateTime
- 更新 AssessmentRecord 的 Status 为 4已完成、设置 CompleteTime 为当前时间
- 异常时回滚事务Status 保持为 3
- _Requirements: 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 9.1_
- [ ]* 4.2 编写属性测试:结果持久化完整性
- **Property 8: 结果持久化完整性**
- 验证写入 AssessmentResult 的记录与内存计算值一一对应
- **Validates: Requirements 5.1, 4.2**
- [ ]* 4.3 编写属性测试:重新生成幂等性
- **Property 9: 重新生成幂等性**
- 验证重新执行报告生成后,数据库中该 RecordId 的结果完全等于最新计算结果,无旧数据残留
- **Validates: Requirements 5.3**
- [ ]* 4.4 编写属性测试:生成完成后状态与时间一致性
- **Property 10: 生成完成后状态与时间一致性**
- 验证成功完成后 Status=4CompleteTime 非空且不早于生成开始时间
- **Validates: Requirements 6.1, 6.2**
- [ ] 5. 服务集成与 DI 注册
- [ ] 5.1 在 ServiceModule 中注册 ReportGenerationService
- 在 `MiAssessment.Infrastructure/Modules/ServiceModule.cs` 中添加 ReportGenerationService 的 Autofac 注册
- 注入 MiAssessmentDbContext 和 ILogger<ReportGenerationService>,使用 InstancePerLifetimeScope
- _Requirements: 7.2_
- [ ] 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 - 确保所有测试通过
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- All code uses C# (.NET 10),遵循项目开发规范中的命名和注释标准
- 使用 Autofac 进行依赖注入注册,与现有项目架构一致