26 KiB
设计文档:测评报告网页版(PDF报告生成用)
概述
测评报告网页版是学业邑规划测评系统的服务端渲染模块,集成在 MiAssessment.Api 项目中,使用 ASP.NET Core Razor Pages 渲染报告 HTML 页面。该模块为后端截图服务提供固定尺寸(1309×926px)的报告网页,截图后与静态图片按配置顺序拼装为 PDF 报告文件。
核心职责
- 结论数据管理:报告生成时从
report_conclusions模板表复制结论到assessment_record_conclusions记录级别表,支持管理员针对单条记录手动调整 - 报告数据服务:提供内部服务方法,一次性查询指定测评记录的完整报告数据
- 服务端渲染:通过 Razor Pages 将报告数据嵌入 HTML 输出,每个报告板块对应独立 URL
- 页面配置:通过
report_page_configs表定义 PDF 报告的页面组成和顺序
设计决策
- 集成到 MiAssessment.Api:报告页面面向外部访问(截图服务通过 HTTP 访问),与小程序 API 共用同一个 Web 项目,复用已有的数据库连接和 DI 配置
- 使用 Razor Pages 而非 MVC Views:Razor Pages 是 ASP.NET Core 推荐的页面渲染方式,每个页面自包含(.cshtml + PageModel),适合报告这种独立页面场景
- ECharts CDN 引入:图表库通过 CDN
<script>标签引入,避免增加后端项目的静态资源体积 - 结论数据独立副本:每条测评记录拥有独立的结论数据,管理员调整不影响模板和其他记录,支持"重新生成"恢复到模板数据
- 新增实体放在 MiAssessment.Model:
AssessmentRecordConclusion和ReportPageConfig实体在 Model 项目中定义,供 Api 和 Admin.Business 共用 - 先搭框架后做页面:本设计聚焦框架层面(数据表、服务、路由、公共模板),具体每个页面的样式布局后续逐个实现
架构
整体数据流
sequenceDiagram
participant RGS as ReportGenerationService
participant DB as Business 数据库
participant RDS as ReportDataService
participant RP as Razor Pages
participant SS as 截图服务(外部)
Note over RGS,DB: 报告生成阶段
RGS->>DB: 计算得分/排名/星级 → 写入 assessment_results
RGS->>DB: 从 report_conclusions 复制 → 写入 assessment_record_conclusions
RGS->>DB: 更新 assessment_records.Status = 4
Note over SS,RP: 截图阶段
SS->>DB: 读取 report_page_configs(按 SortOrder 排序)
loop 每个 PageType=2 的页面
SS->>RP: GET /report/{page-name}?recordId={id}
RP->>RDS: GetReportDataAsync(recordId)
RDS->>DB: 查询 assessment_records + assessment_results + assessment_record_conclusions
RDS-->>RP: ReportDataDto
RP-->>SS: 渲染完成的 HTML(含 data-render-complete 标记)
SS->>SS: 截图 1309×926px
end
SS->>SS: 按 SortOrder 拼装静态图片 + 截图 → PDF
项目集成方案
MiAssessment.Model/
├── Entities/
│ ├── AssessmentRecordConclusion.cs ← 新增实体
│ └── ReportPageConfig.cs ← 新增实体
└── Data/
└── MiAssessmentDbContext.cs ← 新增 DbSet
MiAssessment.Core/
└── Services/
├── ReportGenerationService.cs ← 修改:生成报告时复制结论数据
└── ReportDataService.cs ← 新增:报告数据查询服务
MiAssessment.Api/
├── Pages/ ← 新增 Razor Pages 目录
│ └── Report/
│ ├── _ReportLayout.cshtml ← 报告公共布局
│ ├── Cover.cshtml ← 封面页
│ ├── IntelligenceOverview.cshtml ← 八大智能分析
│ ├── StrongestIntelligence.cshtml ← 最强智能详情
│ ├── WeakestIntelligence.cshtml ← 较弱智能详情
│ ├── PersonalityTraits.cshtml ← 个人特质分析
│ ├── SubAbilities.cshtml ← 40项细分能力
│ ├── LearningTypes.cshtml ← 先天学习类型
│ ├── LearningAbilities.cshtml ← 学习关键能力
│ ├── BrainTypes.cshtml ← 科学大脑类型
│ ├── CharacterTypes.cshtml ← 性格类型
│ └── FutureAbilities.cshtml ← 未来关键发展能力
├── wwwroot/
│ └── css/
│ └── report.css ← 报告公共样式
└── Program.cs ← 添加 Razor Pages 服务注册
组件与接口
1. ReportDataService(报告数据服务)
位于 MiAssessment.Core/Services/,注册为 Scoped 服务。
/// <summary>
/// 报告数据服务接口
/// </summary>
public interface IReportDataService
{
/// <summary>
/// 获取指定测评记录的完整报告数据
/// </summary>
/// <param name="recordId">测评记录ID</param>
/// <returns>报告数据传输对象</returns>
/// <exception cref="BusinessException">记录不存在或状态不正确时抛出</exception>
Task<ReportDataDto> GetReportDataAsync(long recordId);
/// <summary>
/// 确保指定测评记录的结论数据存在,不存在则自动生成
/// </summary>
/// <param name="recordId">测评记录ID</param>
Task EnsureConclusionsExistAsync(long recordId);
}
2. ReportGenerationService 修改
在现有 PersistResultsAsync 方法的事务中,增加结论数据复制逻辑:
// 在写入 AssessmentResult 之后,复制结论数据
// 1. 删除该 RecordId 已有的 AssessmentRecordConclusion 记录
// 2. 根据每个 FinalCategoryResult 的 CategoryId 和 ConclusionType
// 从 report_conclusions 查询模板,复制 Title 和 Content 到 assessment_record_conclusions
3. Razor Pages PageModel 基类
/// <summary>
/// 报告页面基类,提供公共的 recordId 解析和数据加载逻辑
/// </summary>
public abstract class ReportPageModelBase : PageModel
{
protected readonly IReportDataService ReportDataService;
/// <summary>
/// 报告数据(所有页面共用)
/// </summary>
public ReportDataDto? ReportData { get; set; }
/// <summary>
/// 错误信息(数据异常时设置)
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 是否渲染成功
/// </summary>
public bool IsSuccess => ErrorMessage == null && ReportData != null;
protected ReportPageModelBase(IReportDataService reportDataService)
{
ReportDataService = reportDataService;
}
/// <summary>
/// 加载报告数据,处理公共的参数验证和异常捕获
/// </summary>
public async Task<IActionResult> OnGetAsync(long? recordId)
{
if (recordId == null || recordId <= 0)
{
ErrorMessage = "缺少测评记录参数";
return Page();
}
try
{
ReportData = await ReportDataService.GetReportDataAsync(recordId.Value);
await OnDataLoadedAsync(); // 子类可覆写,做页面特定的数据处理
return Page();
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
return Page();
}
}
/// <summary>
/// 数据加载完成后的回调,子类可覆写
/// </summary>
protected virtual Task OnDataLoadedAsync() => Task.CompletedTask;
}
4. 报告页面路由映射
| 路由路径 | Razor Page 文件 | 说明 |
|---|---|---|
/report/cover |
Pages/Report/Cover.cshtml |
封面页 |
/report/intelligence-overview |
Pages/Report/IntelligenceOverview.cshtml |
八大智能分析 |
/report/strongest-intelligence |
Pages/Report/StrongestIntelligence.cshtml |
最强智能详情 |
/report/weakest-intelligence |
Pages/Report/WeakestIntelligence.cshtml |
较弱智能详情 |
/report/personality-traits |
Pages/Report/PersonalityTraits.cshtml |
个人特质分析 |
/report/sub-abilities |
Pages/Report/SubAbilities.cshtml |
40项细分能力 |
/report/learning-types |
Pages/Report/LearningTypes.cshtml |
先天学习类型 |
/report/learning-abilities |
Pages/Report/LearningAbilities.cshtml |
学习关键能力 |
/report/brain-types |
Pages/Report/BrainTypes.cshtml |
科学大脑类型 |
/report/character-types |
Pages/Report/CharacterTypes.cshtml |
性格类型 |
/report/future-abilities |
Pages/Report/FutureAbilities.cshtml |
未来关键发展能力 |
Razor Pages 路由通过 @page "/report/cover" 指令配置,查询参数 recordId 通过 [BindProperty(SupportsGet = true)] 或 OnGetAsync 参数绑定。
5. 公共布局模板 _ReportLayout.cshtml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1309" />
<title>@ViewData["Title"] - 测评报告</title>
<link rel="stylesheet" href="/css/report.css" />
@RenderSection("Styles", required: false)
</head>
<body>
<div class="report-page">
<!-- 页面头部:板块标题 -->
@if (ViewData["PageTitle"] != null)
{
<div class="report-header">
<h1 class="report-title">@ViewData["PageTitle"]</h1>
</div>
}
<!-- 页面主体内容 -->
<div class="report-body">
@RenderBody()
</div>
<!-- 页面底部:页码 -->
@if (ViewData["PageNumber"] != null)
{
<div class="report-footer">
<span class="page-number">@ViewData["PageNumber"]</span>
</div>
}
</div>
@RenderSection("Scripts", required: false)
<!-- 渲染完成标记脚本 -->
<script>
// 默认标记渲染完成(无异步内容的页面)
// 有图表的页面在 Scripts section 中覆盖此逻辑
if (!window.__deferRenderComplete) {
document.body.setAttribute('data-render-complete', 'true');
}
</script>
</body>
</html>
6. 公共样式 report.css 核心规则
/* 页面容器:固定 1309×926px */
.report-page {
width: 1309px;
height: 926px;
margin: 0;
padding: 40px 50px;
box-sizing: border-box;
background-color: #FFFFFF;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
position: relative;
overflow: hidden;
}
/* 页面头部 */
.report-header {
margin-bottom: 20px;
}
.report-title {
font-size: 28px;
font-weight: 600;
color: #333333;
}
/* 页面底部页码 */
.report-footer {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
}
/* 星级显示 */
.star-rating .star-filled { color: #FAAD14; }
.star-rating .star-empty { color: #E8E8E8; }
/* 错误页面 */
.report-error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #FF4D4F;
font-size: 20px;
}
/* 主色调 */
:root {
--primary-color: #4A90E2;
--text-color: #333333;
--text-secondary: #666666;
--border-color: #E8E8E8;
--bg-gray: #F8F8F8;
}
数据模型
1. assessment_record_conclusions 表
存储每条测评记录的独立结论数据副本,由报告生成时从 report_conclusions 模板表复制而来。
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| Id | bigint | PK, 自增 | 主键ID |
| RecordId | bigint | NOT NULL, FK → assessment_records.Id | 测评记录ID |
| CategoryId | bigint | NOT NULL, FK → report_categories.Id | 分类ID |
| ConclusionType | int | NOT NULL | 结论类型:1最强 2较强 3较弱 4最弱 |
| StarLevel | int | NOT NULL | 星级(1-5),用于记录级别的星级覆盖 |
| Title | nvarchar(100) | NULL | 结论标题 |
| Content | nvarchar(max) | NOT NULL | 结论内容(富文本) |
| CreateTime | datetime | NOT NULL, DEFAULT getdate() | 创建时间 |
| UpdateTime | datetime | NOT NULL, DEFAULT getdate() | 更新时间 |
| IsDeleted | bit | NOT NULL, DEFAULT 0 | 软删除标记 |
索引:
ix_arc_record_idON (RecordId) — 按测评记录查询ix_arc_record_categoryON (RecordId, CategoryId) — 按记录+分类查询
对应实体类 AssessmentRecordConclusion:
[Table("assessment_record_conclusions")]
public class AssessmentRecordConclusion
{
[Key]
public long Id { get; set; }
public long RecordId { get; set; }
public long CategoryId { get; set; }
/// <summary>
/// 结论类型:1最强 2较强 3较弱 4最弱
/// </summary>
public int ConclusionType { get; set; }
/// <summary>
/// 星级(1-5),记录级别的星级,可由管理员覆盖
/// </summary>
public int StarLevel { 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(RecordId))]
public virtual AssessmentRecord? Record { get; set; }
[ForeignKey(nameof(CategoryId))]
public virtual ReportCategory? Category { get; set; }
}
2. report_page_configs 表
定义 PDF 报告中每一页的类型、顺序和关联资源。
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| Id | bigint | PK, 自增 | 主键ID |
| PageType | int | NOT NULL | 页面类型:1静态图片 2网页截图 |
| PageName | nvarchar(50) | NOT NULL | 页面标识名称(如 cover、intelligence-overview) |
| Title | nvarchar(100) | NOT NULL | 页面显示标题 |
| SortOrder | int | NOT NULL | 排序序号,从 1 开始 |
| ImageUrl | nvarchar(500) | NULL | 静态图片路径(PageType=1 时必填) |
| RouteUrl | nvarchar(200) | NULL | 网页路由路径(PageType=2 时必填) |
| Status | int | NOT NULL, DEFAULT 1 | 状态:0禁用 1启用 |
| CreateTime | datetime | NOT NULL, DEFAULT getdate() | 创建时间 |
| UpdateTime | datetime | NOT NULL, DEFAULT getdate() | 更新时间 |
索引:
ix_rpc_sort_orderON (SortOrder) — 按排序查询ix_rpc_statusON (Status) — 按状态筛选
对应实体类 ReportPageConfig:
[Table("report_page_configs")]
public class ReportPageConfig
{
[Key]
public long Id { get; set; }
/// <summary>
/// 页面类型:1静态图片 2网页截图
/// </summary>
public int PageType { get; set; }
/// <summary>
/// 页面标识名称
/// </summary>
[Required]
[MaxLength(50)]
public string PageName { get; set; } = null!;
/// <summary>
/// 页面显示标题
/// </summary>
[Required]
[MaxLength(100)]
public string Title { get; set; } = null!;
/// <summary>
/// 排序序号
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 静态图片路径(PageType=1 时使用)
/// </summary>
[MaxLength(500)]
public string? ImageUrl { get; set; }
/// <summary>
/// 网页路由路径(PageType=2 时使用)
/// </summary>
[MaxLength(200)]
public string? RouteUrl { get; set; }
/// <summary>
/// 状态:0禁用 1启用
/// </summary>
public int Status { get; set; } = 1;
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
}
3. ReportDataDto(报告数据传输对象)
/// <summary>
/// 报告完整数据 DTO
/// </summary>
public class ReportDataDto
{
/// <summary>
/// 测评记录基本信息
/// </summary>
public RecordInfoDto RecordInfo { get; set; } = null!;
/// <summary>
/// 按 CategoryType 分组的测评结果
/// Key: CategoryType (1-8), Value: 该类型下所有分类的结果列表
/// </summary>
public Dictionary<int, List<CategoryResultDataDto>> ResultsByType { get; set; } = new();
/// <summary>
/// 按 CategoryId 索引的结论数据
/// Key: CategoryId, Value: 该分类的结论数据
/// </summary>
public Dictionary<long, ConclusionDataDto> ConclusionsByCategory { get; set; } = new();
/// <summary>
/// 报告分类层级结构
/// Key: CategoryType, Value: 该类型的分类树
/// </summary>
public Dictionary<int, List<CategoryTreeDto>> CategoryTrees { get; set; } = new();
}
/// <summary>
/// 测评记录基本信息
/// </summary>
public class RecordInfoDto
{
public long RecordId { get; set; }
public string Name { get; set; } = null!;
public int Gender { get; set; }
public int Age { get; set; }
public int EducationStage { get; set; }
public string Province { get; set; } = null!;
public string City { get; set; } = null!;
public string District { get; set; } = null!;
public DateTime? CompleteTime { get; set; }
}
/// <summary>
/// 分类测评结果数据
/// </summary>
public class CategoryResultDataDto
{
public long CategoryId { get; set; }
public string CategoryName { get; set; } = null!;
public string CategoryCode { get; set; } = null!;
public int CategoryType { get; set; }
public long ParentId { get; set; }
public decimal Score { get; set; }
public decimal MaxScore { get; set; }
public decimal Percentage { get; set; }
public int Rank { get; set; }
public int StarLevel { get; set; }
}
/// <summary>
/// 结论数据
/// </summary>
public class ConclusionDataDto
{
public long CategoryId { get; set; }
public int ConclusionType { get; set; }
public int StarLevel { get; set; }
public string? Title { get; set; }
public string Content { get; set; } = null!;
}
/// <summary>
/// 分类树节点
/// </summary>
public class CategoryTreeDto
{
public long Id { get; set; }
public string Name { get; set; } = null!;
public string Code { get; set; } = null!;
public int CategoryType { get; set; }
public long ParentId { get; set; }
public List<CategoryTreeDto> Children { get; set; } = new();
}
4. 结论数据复制逻辑
在 ReportGenerationService.PersistResultsAsync 事务中增加:
对于每个 FinalCategoryResult:
1. conclusionType = MapStarToConclusionType(starLevel)
2. 从 report_conclusions 查询 (CategoryId, conclusionType) 对应的模板
3. 创建 AssessmentRecordConclusion:
- RecordId = 当前记录ID
- CategoryId = result.CategoryId
- ConclusionType = conclusionType
- StarLevel = result.StarLevel
- Title = 模板.Title
- Content = 模板.Content
4. 批量写入 assessment_record_conclusions
重新生成逻辑:
1. 删除该 RecordId 的所有 AssessmentRecordConclusion(硬删除)
2. 删除该 RecordId 的所有 AssessmentResult(硬删除)
3. 重新执行完整的报告生成流水线(计算得分 → 排名 → 星级 → 匹配结论 → 复制结论 → 写入结果)
5. Program.cs 集成变更
// 添加 Razor Pages 支持
builder.Services.AddRazorPages();
// 在 app.MapControllers() 之前添加
app.MapRazorPages();
// 添加静态文件支持(用于 report.css)
app.UseStaticFiles();
正确性属性(Correctness Properties)
正确性属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。
Property 1: 结论复制数据一致性
For any 已完成的测评记录和其对应的 AssessmentResult 列表,执行结论复制后,每条 AssessmentRecordConclusion 的 ConclusionType 应等于 MapStarToConclusionType(StarLevel),且 Title 和 Content 应与 report_conclusions 模板表中对应 (CategoryId, ConclusionType) 的记录完全一致。
Validates: Requirements 1.1, 1.3
Property 2: 重新生成报告的幂等性
For any 测评记录,执行"重新生成报告"后产生的 AssessmentResult 和 AssessmentRecordConclusion 数据,应与对同一份原始答案数据执行首次生成的结果完全一致(即手动调整被完全覆盖,恢复到模板状态)。
Validates: Requirements 1.6, 1.7
Property 3: 结论数据自动生成
For any 状态为 4(已完成)的测评记录,如果其对应的 AssessmentRecordConclusion 记录不存在,调用 ReportDataService.GetReportDataAsync 后,该记录的 AssessmentRecordConclusion 应被自动创建,且数据与从模板复制的结果一致。
Validates: Requirements 1.9
Property 4: recordId 参数验证
For any 报告页面路由,当 URL 中缺少 recordId 参数或 recordId 为空/无效时,返回的 HTML 应包含"缺少测评记录参数"错误提示文本,且 document.body 上应有 data-render-error="true" 属性。
Validates: Requirements 2.3, 2.6, 18.1
Property 5: 报告数据完整性
For any 状态为 4 的测评记录,GetReportDataAsync 返回的 ReportDataDto 应包含:非空的 RecordInfo(含姓名、性别、年龄、学业阶段、省市区、完成时间),以及 CategoryType 1-8 中每个有对应 AssessmentResult 的类型都存在于 ResultsByType 字典中。
Validates: Requirements 3.2
Property 6: 页面配置类型字段约束
For any ReportPageConfig 记录,如果 PageType=1 则 ImageUrl 不为空,如果 PageType=2 则 RouteUrl 不为空。
Validates: Requirements 4.2, 4.3
Property 7: 页面配置排序与过滤
For any report_page_configs 记录集合,生成 PDF 时使用的页面列表应仅包含 Status=1 的记录,且按 SortOrder 升序排列。
Validates: Requirements 4.4, 4.5
Property 8: 渲染完成信号
For any 成功渲染的报告页面(无数据异常),输出的 HTML 中 document.body 应包含 data-render-complete="true" 属性。
Validates: Requirements 5.4, 18.3
Property 9: 星级图标渲染正确性
For any 星级值 n(1 ≤ n ≤ 5),渲染的星级 HTML 应包含恰好 n 个填充星(star-filled)和 5-n 个空心星(star-empty)。
Validates: Requirements 17.4
Property 10: 空结论占位文本
For any 报告页面中某个分类的 AssessmentRecordConclusion 结论文本为空或不存在时,该位置的渲染输出应包含"暂无分析内容"占位文本,且不影响页面其他内容的正常展示。
Validates: Requirements 18.2
错误处理
数据层错误
| 场景 | 处理方式 |
|---|---|
| recordId 对应的记录不存在或已软删除 | ReportDataService 抛出 BusinessException("测评记录不存在") |
| 记录状态不为 4(已完成) | ReportDataService 抛出 BusinessException("报告尚未生成完成") |
| 结论数据不存在 | 自动触发 EnsureConclusionsExistAsync 生成结论数据 |
| 数据库连接异常 | 记录日志,页面显示通用错误信息 |
渲染层错误
| 场景 | 处理方式 |
|---|---|
| URL 缺少 recordId 参数 | 返回包含"缺少测评记录参数"的错误 HTML,body 添加 data-render-error="true" |
| ReportDataService 抛出异常 | 捕获异常,页面显示错误信息,body 添加 data-render-error="true" |
| 某个板块的结论文本为空 | 对应位置显示"暂无分析内容"占位文本,不影响其他内容 |
| ECharts CDN 加载失败 | 图表区域显示降级文本,仍然标记 data-render-complete="true" |
页面配置错误
| 场景 | 处理方式 |
|---|---|
| report_page_configs 表无启用记录 | 截图服务返回错误"报告页面配置为空,无法生成 PDF" |
| PageType=1 但 ImageUrl 为空 | 跳过该页,记录警告日志 |
| PageType=2 但 RouteUrl 为空 | 跳过该页,记录警告日志 |
测试策略
测试框架
- 单元测试:xUnit + Moq
- 属性测试:FsCheck(.NET 属性测试库)
- 集成测试:WebApplicationFactory(ASP.NET Core 集成测试)
单元测试
重点覆盖以下场景:
-
ReportDataService
- GetReportDataAsync 正常返回完整数据
- recordId 不存在时抛出异常
- 记录状态非 4 时抛出异常
- 结论不存在时自动触发生成
-
结论复制逻辑
- 星级到结论类型的映射(已有测试,MapStarToConclusionType)
- 复制后数据与模板一致
- 重新生成覆盖手动调整
-
Razor Pages 渲染
- 缺少 recordId 时显示错误页面
- 正常数据渲染包含 data-render-complete 属性
- 空结论显示占位文本
-
页面配置
- 按 SortOrder 排序
- Status=0 的记录被过滤
- 空配置返回错误
属性测试
每个属性测试最少运行 100 次迭代,使用 FsCheck 生成随机输入。
| 属性 | 测试标签 | 说明 |
|---|---|---|
| Property 1 | Feature: report-web-pages, Property 1: 结论复制数据一致性 | 生成随机星级和分类,验证复制后的 ConclusionType 和内容匹配 |
| Property 2 | Feature: report-web-pages, Property 2: 重新生成报告的幂等性 | 生成随机答案数据,验证两次生成结果一致 |
| Property 5 | Feature: report-web-pages, Property 5: 报告数据完整性 | 生成随机完成记录,验证返回数据包含所有必要字段 |
| Property 6 | Feature: report-web-pages, Property 6: 页面配置类型字段约束 | 生成随机 PageConfig,验证 PageType 与对应字段的约束 |
| Property 7 | Feature: report-web-pages, Property 7: 页面配置排序与过滤 | 生成随机配置列表,验证过滤和排序结果 |
| Property 9 | Feature: report-web-pages, Property 9: 星级图标渲染正确性 | 生成随机星级值 1-5,验证填充星和空心星数量 |
集成测试
使用 WebApplicationFactory<Program> 进行端到端测试:
- 启动测试服务器,访问各报告页面路由
- 验证 HTTP 200 响应和 HTML 内容
- 验证错误场景的 HTML 输出
- 验证 data-render-complete / data-render-error 属性