From d2a4f01e50ee74f27843c984504ba4a2d350802c Mon Sep 17 00:00:00 2001 From: zpc Date: Wed, 25 Feb 2026 17:32:05 +0800 Subject: [PATCH] 21 --- .kiro/specs/report-web-pages/design.md | 733 ++++++++++++++++++ .kiro/specs/report-web-pages/tasks.md | 237 ++++++ .../Pages/Report/BrainTypes.cshtml | 19 + .../Pages/Report/BrainTypes.cshtml.cs | 14 + .../Pages/Report/CharacterTypes.cshtml | 19 + .../Pages/Report/CharacterTypes.cshtml.cs | 14 + .../Pages/Report/Cover.cshtml | 24 + .../Pages/Report/Cover.cshtml.cs | 14 + .../Pages/Report/FutureAbilities.cshtml | 19 + .../Pages/Report/FutureAbilities.cshtml.cs | 14 + .../Pages/Report/IntelligenceOverview.cshtml | 19 + .../Report/IntelligenceOverview.cshtml.cs | 14 + .../Pages/Report/LearningAbilities.cshtml | 19 + .../Pages/Report/LearningAbilities.cshtml.cs | 14 + .../Pages/Report/LearningTypes.cshtml | 19 + .../Pages/Report/LearningTypes.cshtml.cs | 14 + .../Pages/Report/PersonalityTraits.cshtml | 19 + .../Pages/Report/PersonalityTraits.cshtml.cs | 14 + .../Pages/Report/ReportPageModelBase.cs | 63 ++ .../Pages/Report/StrongestIntelligence.cshtml | 19 + .../Report/StrongestIntelligence.cshtml.cs | 14 + .../Pages/Report/SubAbilities.cshtml | 19 + .../Pages/Report/SubAbilities.cshtml.cs | 14 + .../Pages/Report/WeakestIntelligence.cshtml | 19 + .../Report/WeakestIntelligence.cshtml.cs | 14 + .../Pages/Report/_ReportLayout.cshtml | 45 ++ .../Pages/Report/_ViewStart.cshtml | 3 + .../Pages/_ViewImports.cshtml | 3 + .../src/MiAssessment.Api/Program.cs | 9 + .../MiAssessment.Api/wwwroot/css/report.css | 150 ++++ .../Interfaces/IReportDataService.cs | 38 + .../Services/ReportDataService.cs | 274 +++++++ .../Services/ReportGenerationService.cs | 67 +- .../Modules/ServiceModule.cs | 10 + .../Data/MiAssessmentDbContext.cs | 80 ++ .../Entities/AssessmentRecordConclusion.cs | 78 ++ .../Entities/ReportPageConfig.cs | 69 ++ .../Models/Report/ReportDataDto.cs | 204 +++++ temp_sql/batch18_report_web_pages.sql | 144 ++++ 39 files changed, 2574 insertions(+), 1 deletion(-) create mode 100644 .kiro/specs/report-web-pages/design.md create mode 100644 .kiro/specs/report-web-pages/tasks.md create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/ReportPageModelBase.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml.cs create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ReportLayout.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ViewStart.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/Pages/_ViewImports.cshtml create mode 100644 server/MiAssessment/src/MiAssessment.Api/wwwroot/css/report.css create mode 100644 server/MiAssessment/src/MiAssessment.Core/Interfaces/IReportDataService.cs create mode 100644 server/MiAssessment/src/MiAssessment.Core/Services/ReportDataService.cs create mode 100644 server/MiAssessment/src/MiAssessment.Model/Entities/AssessmentRecordConclusion.cs create mode 100644 server/MiAssessment/src/MiAssessment.Model/Entities/ReportPageConfig.cs create mode 100644 server/MiAssessment/src/MiAssessment.Model/Models/Report/ReportDataDto.cs create mode 100644 temp_sql/batch18_report_web_pages.sql diff --git a/.kiro/specs/report-web-pages/design.md b/.kiro/specs/report-web-pages/design.md new file mode 100644 index 0000000..1e09598 --- /dev/null +++ b/.kiro/specs/report-web-pages/design.md @@ -0,0 +1,733 @@ +# 设计文档:测评报告网页版(PDF报告生成用) + +## 概述 + +测评报告网页版是学业邑规划测评系统的服务端渲染模块,集成在 MiAssessment.Api 项目中,使用 ASP.NET Core Razor Pages 渲染报告 HTML 页面。该模块为后端截图服务提供固定尺寸(1309×926px)的报告网页,截图后与静态图片按配置顺序拼装为 PDF 报告文件。 + +### 核心职责 + +1. **结论数据管理**:报告生成时从 `report_conclusions` 模板表复制结论到 `assessment_record_conclusions` 记录级别表,支持管理员针对单条记录手动调整 +2. **报告数据服务**:提供内部服务方法,一次性查询指定测评记录的完整报告数据 +3. **服务端渲染**:通过 Razor Pages 将报告数据嵌入 HTML 输出,每个报告板块对应独立 URL +4. **页面配置**:通过 `report_page_configs` 表定义 PDF 报告的页面组成和顺序 + +### 设计决策 + +- **集成到 MiAssessment.Api**:报告页面面向外部访问(截图服务通过 HTTP 访问),与小程序 API 共用同一个 Web 项目,复用已有的数据库连接和 DI 配置 +- **使用 Razor Pages 而非 MVC Views**:Razor Pages 是 ASP.NET Core 推荐的页面渲染方式,每个页面自包含(.cshtml + PageModel),适合报告这种独立页面场景 +- **ECharts CDN 引入**:图表库通过 CDN ` + + +``` + +### 6. 公共样式 `report.css` 核心规则 + +```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_id` ON (RecordId) — 按测评记录查询 +- `ix_arc_record_category` ON (RecordId, CategoryId) — 按记录+分类查询 + +对应实体类 `AssessmentRecordConclusion`: + +```csharp +[Table("assessment_record_conclusions")] +public class AssessmentRecordConclusion +{ + [Key] + public long Id { get; set; } + + public long RecordId { get; set; } + + public long CategoryId { get; set; } + + /// + /// 结论类型:1最强 2较强 3较弱 4最弱 + /// + public int ConclusionType { get; set; } + + /// + /// 星级(1-5),记录级别的星级,可由管理员覆盖 + /// + 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_order` ON (SortOrder) — 按排序查询 +- `ix_rpc_status` ON (Status) — 按状态筛选 + +对应实体类 `ReportPageConfig`: + +```csharp +[Table("report_page_configs")] +public class ReportPageConfig +{ + [Key] + public long Id { get; set; } + + /// + /// 页面类型:1静态图片 2网页截图 + /// + public int PageType { get; set; } + + /// + /// 页面标识名称 + /// + [Required] + [MaxLength(50)] + public string PageName { get; set; } = null!; + + /// + /// 页面显示标题 + /// + [Required] + [MaxLength(100)] + public string Title { get; set; } = null!; + + /// + /// 排序序号 + /// + public int SortOrder { get; set; } + + /// + /// 静态图片路径(PageType=1 时使用) + /// + [MaxLength(500)] + public string? ImageUrl { get; set; } + + /// + /// 网页路由路径(PageType=2 时使用) + /// + [MaxLength(200)] + public string? RouteUrl { get; set; } + + /// + /// 状态:0禁用 1启用 + /// + public int Status { get; set; } = 1; + + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } +} +``` + +### 3. ReportDataDto(报告数据传输对象) + +```csharp +/// +/// 报告完整数据 DTO +/// +public class ReportDataDto +{ + /// + /// 测评记录基本信息 + /// + public RecordInfoDto RecordInfo { get; set; } = null!; + + /// + /// 按 CategoryType 分组的测评结果 + /// Key: CategoryType (1-8), Value: 该类型下所有分类的结果列表 + /// + public Dictionary> ResultsByType { get; set; } = new(); + + /// + /// 按 CategoryId 索引的结论数据 + /// Key: CategoryId, Value: 该分类的结论数据 + /// + public Dictionary ConclusionsByCategory { get; set; } = new(); + + /// + /// 报告分类层级结构 + /// Key: CategoryType, Value: 该类型的分类树 + /// + public Dictionary> CategoryTrees { get; set; } = new(); +} + +/// +/// 测评记录基本信息 +/// +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; } +} + +/// +/// 分类测评结果数据 +/// +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; } +} + +/// +/// 结论数据 +/// +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!; +} + +/// +/// 分类树节点 +/// +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 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 集成变更 + +```csharp +// 添加 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 集成测试) + +### 单元测试 + +重点覆盖以下场景: + +1. **ReportDataService** + - GetReportDataAsync 正常返回完整数据 + - recordId 不存在时抛出异常 + - 记录状态非 4 时抛出异常 + - 结论不存在时自动触发生成 + +2. **结论复制逻辑** + - 星级到结论类型的映射(已有测试,MapStarToConclusionType) + - 复制后数据与模板一致 + - 重新生成覆盖手动调整 + +3. **Razor Pages 渲染** + - 缺少 recordId 时显示错误页面 + - 正常数据渲染包含 data-render-complete 属性 + - 空结论显示占位文本 + +4. **页面配置** + - 按 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` 进行端到端测试: + +1. 启动测试服务器,访问各报告页面路由 +2. 验证 HTTP 200 响应和 HTML 内容 +3. 验证错误场景的 HTML 输出 +4. 验证 data-render-complete / data-render-error 属性 diff --git a/.kiro/specs/report-web-pages/tasks.md b/.kiro/specs/report-web-pages/tasks.md new file mode 100644 index 0000000..e8e68c0 --- /dev/null +++ b/.kiro/specs/report-web-pages/tasks.md @@ -0,0 +1,237 @@ +# 实现计划:测评报告网页版框架搭建 + +## 概述 + +本任务聚焦于报告网页版的框架层面搭建:数据库表与实体类、数据服务、Razor Pages 基础设施、公共布局模板和样式。具体 11 个报告页面的样式和内容实现不在本次范围内,后续逐个开发。 + +## 任务 + +- [x] 1. 新增数据库实体类和 DbContext 配置 + - [x] 1.1 创建 AssessmentRecordConclusion 实体类 + - 在 `MiAssessment.Model/Entities/` 下新建 `AssessmentRecordConclusion.cs` + - 包含字段:Id、RecordId、CategoryId、ConclusionType、StarLevel、Title、Content、CreateTime、UpdateTime、IsDeleted + - 使用 `[Table("assessment_record_conclusions")]` 映射表名 + - 添加 ForeignKey 导航属性关联 AssessmentRecord 和 ReportCategory + - 所有属性添加 XML 注释 + - _需求: 1.2, 1.5_ + + - [x] 1.2 创建 ReportPageConfig 实体类 + - 在 `MiAssessment.Model/Entities/` 下新建 `ReportPageConfig.cs` + - 包含字段:Id、PageType、PageName、Title、SortOrder、ImageUrl、RouteUrl、Status、CreateTime、UpdateTime + - 使用 `[Table("report_page_configs")]` 映射表名 + - 所有属性添加 XML 注释 + - _需求: 4.1_ + + - [x] 1.3 在 MiAssessmentDbContext 中注册新 DbSet + - 在 `MiAssessment.Model/Data/MiAssessmentDbContext.cs` 中添加 `DbSet` 和 `DbSet` + - 在 `OnModelCreating` 中配置索引:`ix_arc_record_id`、`ix_arc_record_category`、`ix_rpc_sort_order`、`ix_rpc_status` + - _需求: 1.2, 4.1_ + +- [x] 2. 创建数据库迁移 SQL 脚本 + - [x] 2.1 编写 assessment_record_conclusions 建表 SQL + - 在 `temp_sql/` 下创建 SQL 脚本文件 + - 包含表结构、索引、默认值约束 + - _需求: 1.2_ + + - [x] 2.2 编写 report_page_configs 建表 SQL 和初始数据 + - 建表 SQL 包含表结构、索引、默认值约束 + - 插入初始配置数据(11 个网页截图页面 + 静态图片页面占位),按 SortOrder 排序 + - _需求: 4.1, 4.2, 4.3_ + +- [x] 3. 检查点 - 确认实体类和数据库脚本 + - 确保所有实体类编译通过,SQL 脚本语法正确,询问用户是否有问题。 + +- [x] 4. 创建报告数据 DTO 模型 + - [x] 4.1 创建报告数据传输对象 + - 在 `MiAssessment.Model/Models/` 下新建 `Report/` 目录 + - 创建 `ReportDataDto.cs`,包含 ReportDataDto、RecordInfoDto、CategoryResultDataDto、ConclusionDataDto、CategoryTreeDto + - 所有属性添加 XML 注释 + - _需求: 3.1, 3.2_ + +- [x] 5. 实现 ReportDataService(报告数据服务) + - [x] 5.1 创建 IReportDataService 接口 + - 在 `MiAssessment.Core/Interfaces/` 下新建 `IReportDataService.cs` + - 定义 `GetReportDataAsync(long recordId)` 和 `EnsureConclusionsExistAsync(long recordId)` 方法 + - _需求: 3.1, 3.5, 1.9_ + + - [x] 5.2 实现 ReportDataService + - 在 `MiAssessment.Core/Services/` 下新建 `ReportDataService.cs` + - 实现 `GetReportDataAsync`:验证记录存在且状态为 4,查询 assessment_records、assessment_results、assessment_record_conclusions、report_categories,组装 ReportDataDto + - 实现 `EnsureConclusionsExistAsync`:检查结论是否存在,不存在则从 report_conclusions 模板复制生成 + - 记录不存在时抛出 BusinessException("测评记录不存在") + - 状态非 4 时抛出 BusinessException("报告尚未生成完成") + - 结论不存在时自动触发生成 + - _需求: 3.1, 3.2, 3.3, 3.4, 3.5, 1.9_ + + - [ ]* 5.3 编写 ReportDataService 单元测试 + - 测试 GetReportDataAsync 正常返回完整数据 + - 测试 recordId 不存在时抛出异常 + - 测试记录状态非 4 时抛出异常 + - 测试结论不存在时自动触发生成 + - _需求: 3.3, 3.4, 1.9_ + +- [x] 6. 修改 ReportGenerationService 增加结论复制逻辑 + - [x] 6.1 在 PersistResultsAsync 中添加结论数据复制 + - 在现有事务中,写入 AssessmentResult 之后,增加以下逻辑: + - 删除该 RecordId 已有的 AssessmentRecordConclusion 记录(硬删除,支持重新生成) + - 根据每个 FinalCategoryResult 的 StarLevel 调用 MapStarToConclusionType 确定 ConclusionType + - 从 report_conclusions 查询对应 (CategoryId, ConclusionType) 的模板记录 + - 复制 Title 和 Content 到新的 AssessmentRecordConclusion,设置 RecordId、CategoryId、ConclusionType、StarLevel + - 批量写入 assessment_record_conclusions + - _需求: 1.1, 1.3, 1.6, 1.7_ + + - [ ]* 6.2 编写结论复制逻辑单元测试 + - 测试复制后 ConclusionType 与 MapStarToConclusionType 映射一致 + - 测试复制后 Title 和 Content 与模板一致 + - 测试重新生成时旧结论被删除 + - _需求: 1.1, 1.3, 1.6_ + + - [ ]* 6.3 编写属性测试:结论复制数据一致性 + - **Property 1: 结论复制数据一致性** + - **验证: 需求 1.1, 1.3** + - 生成随机星级和分类,验证复制后的 ConclusionType 和内容与模板匹配 + + - [ ]* 6.4 编写属性测试:重新生成报告的幂等性 + - **Property 2: 重新生成报告的幂等性** + - **验证: 需求 1.6, 1.7** + - 生成随机答案数据,验证两次生成结果完全一致 + +- [x] 7. 检查点 - 确认数据服务层 + - 确保 ReportDataService 和 ReportGenerationService 修改编译通过,所有测试通过,询问用户是否有问题。 + +- [x] 8. 配置 Razor Pages 基础设施 + - [x] 8.1 修改 Program.cs 添加 Razor Pages 支持 + - 在 `builder.Services` 中添加 `builder.Services.AddRazorPages()` + - 在 `app.MapControllers()` 之前添加 `app.MapRazorPages()` + - 添加 `app.UseStaticFiles()` 支持静态文件(report.css) + - _需求: 2.1_ + + - [x] 8.2 创建报告公共布局 _ReportLayout.cshtml + - 在 `MiAssessment.Api/Pages/Report/` 下创建 `_ReportLayout.cshtml` + - 固定页面尺寸 1309×926px,白色背景 + - 包含页面头部(板块标题)、主体内容区、底部页码 + - 包含 `report.css` 引用 + - 包含 Styles 和 Scripts 可选 Section + - 包含渲染完成标记脚本(`data-render-complete="true"`) + - 包含错误状态标记逻辑(`data-render-error="true"`) + - _需求: 2.1, 5.1, 5.2, 5.3, 5.4, 17.5, 18.1, 18.3_ + + - [x] 8.3 创建 _ViewImports.cshtml 和 _ViewStart.cshtml + - 在 `MiAssessment.Api/Pages/` 下创建 `_ViewImports.cshtml`,添加 `@using` 和 `@addTagHelper` 指令 + - 在 `MiAssessment.Api/Pages/Report/` 下创建 `_ViewStart.cshtml`,指定使用 `_ReportLayout` 布局 + - _需求: 2.1_ + + - [x] 8.4 创建 ReportPageModelBase 基类 + - 在 `MiAssessment.Api/Pages/Report/` 下创建 `ReportPageModelBase.cs` + - 继承 `PageModel`,注入 `IReportDataService` + - 实现公共的 `OnGetAsync(long? recordId)` 方法:参数验证、数据加载、异常捕获 + - 提供 `ReportData`、`ErrorMessage`、`IsSuccess` 属性 + - 提供 `OnDataLoadedAsync()` 虚方法供子类覆写 + - _需求: 2.3, 2.6, 18.1_ + +- [x] 9. 创建报告公共样式 + - [x] 9.1 创建 report.css 公共样式文件 + - 在 `MiAssessment.Api/wwwroot/css/` 下创建 `report.css` + - 页面容器 `.report-page` 固定 1309×926px,白色背景,overflow: hidden + - 页面头部 `.report-header` 和标题样式 + - 页面底部 `.report-footer` 绝对定位页码 + - 星级显示 `.star-rating`(填充星 `.star-filled` 金色,空心星 `.star-empty` 灰色) + - 错误页面 `.report-error` 居中显示 + - 占位文本 `.no-content` 样式 + - CSS 变量定义(主色调、文字颜色、边框颜色等) + - 字体层级按 1309×926px 页面尺寸设计 + - _需求: 5.1, 5.2, 5.3, 17.1, 17.2, 17.3, 17.4, 18.2_ + +- [x] 10. 创建 11 个报告页面骨架 + - [x] 10.1 创建封面页骨架 Cover.cshtml + Cover.cshtml.cs + - 路由 `@page "/report/cover"` + - PageModel 继承 ReportPageModelBase + - cshtml 中使用 `_ReportLayout` 布局,显示基本占位内容(标题、测评人信息区域) + - 具体样式和布局后续实现 + - _需求: 2.4, 2.5, 6.1, 6.2, 6.3, 6.4_ + + - [x] 10.2 创建八大智能分析页骨架 IntelligenceOverview.cshtml + IntelligenceOverview.cshtml.cs + - 路由 `@page "/report/intelligence-overview"` + - PageModel 继承 ReportPageModelBase + - cshtml 中显示基本占位内容(雷达图区域、柱状图区域) + - _需求: 2.4, 2.5, 7.1, 7.2, 7.3, 7.4_ + + - [x] 10.3 创建最强智能详情页骨架 StrongestIntelligence.cshtml + StrongestIntelligence.cshtml.cs + - 路由 `@page "/report/strongest-intelligence"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 8.1, 8.4_ + + - [x] 10.4 创建较弱智能详情页骨架 WeakestIntelligence.cshtml + WeakestIntelligence.cshtml.cs + - 路由 `@page "/report/weakest-intelligence"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 9.1, 9.4_ + + - [x] 10.5 创建个人特质分析页骨架 PersonalityTraits.cshtml + PersonalityTraits.cshtml.cs + - 路由 `@page "/report/personality-traits"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 10.1, 10.4_ + + - [x] 10.6 创建 40 项细分能力分析页骨架 SubAbilities.cshtml + SubAbilities.cshtml.cs + - 路由 `@page "/report/sub-abilities"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 11.1, 11.3_ + + - [x] 10.7 创建先天学习类型分析页骨架 LearningTypes.cshtml + LearningTypes.cshtml.cs + - 路由 `@page "/report/learning-types"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 12.1, 12.4_ + + - [x] 10.8 创建学习关键能力分析页骨架 LearningAbilities.cshtml + LearningAbilities.cshtml.cs + - 路由 `@page "/report/learning-abilities"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 13.1, 13.3_ + + - [x] 10.9 创建科学大脑类型分析页骨架 BrainTypes.cshtml + BrainTypes.cshtml.cs + - 路由 `@page "/report/brain-types"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 14.1, 14.4_ + + - [x] 10.10 创建性格类型分析页骨架 CharacterTypes.cshtml + CharacterTypes.cshtml.cs + - 路由 `@page "/report/character-types"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 15.1, 15.4_ + + - [x] 10.11 创建未来关键发展能力分析页骨架 FutureAbilities.cshtml + FutureAbilities.cshtml.cs + - 路由 `@page "/report/future-abilities"` + - PageModel 继承 ReportPageModelBase + - _需求: 2.4, 2.5, 16.1, 16.3_ + +- [x] 11. 检查点 - 确认 Razor Pages 基础设施 + - 确保所有 Razor Pages 编译通过,路由可访问,公共布局和样式正常加载,询问用户是否有问题。 + +- [x] 12. 服务注册与集成 + - [x] 12.1 注册 ReportDataService 到依赖注入容器 + - 在 Autofac ServiceModule 或 Program.cs 中注册 `IReportDataService` → `ReportDataService`(Scoped 生命周期) + - 确保 Razor Pages 的 PageModel 可以通过构造函数注入 IReportDataService + - _需求: 3.5_ + + - [ ]* 12.2 编写属性测试:报告数据完整性 + - **Property 5: 报告数据完整性** + - **验证: 需求 3.2** + - 生成随机完成记录,验证返回数据包含所有必要字段 + + - [ ]* 12.3 编写属性测试:页面配置类型字段约束 + - **Property 6: 页面配置类型字段约束** + - **验证: 需求 4.2, 4.3** + - 生成随机 PageConfig,验证 PageType 与对应字段的约束 + + - [ ]* 12.4 编写属性测试:星级图标渲染正确性 + - **Property 9: 星级图标渲染正确性** + - **验证: 需求 17.4** + - 生成随机星级值 1-5,验证填充星和空心星数量 + +- [x] 13. 最终检查点 - 确保所有代码编译通过 + - 确保所有测试通过,所有 Razor Pages 路由可正常访问(返回占位内容或错误页面),询问用户是否有问题。 + +## 备注 + +- 标记 `*` 的任务为可选任务,可跳过以加快 MVP 进度 +- 每个任务引用了具体的需求编号,确保可追溯性 +- 检查点用于增量验证,确保每个阶段的代码质量 +- 属性测试验证通用正确性属性,单元测试验证具体场景和边界条件 +- 本次任务仅搭建框架,11 个报告页面的具体样式和内容实现将在后续逐个开发 diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml new file mode 100644 index 0000000..0bfbf44 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml @@ -0,0 +1,19 @@ +@page "/report/brain-types" +@model MiAssessment.Api.Pages.Report.BrainTypesModel +@{ + ViewData["Title"] = "科学大脑类型分析"; + ViewData["PageTitle"] = "科学大脑类型分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

科学大脑类型分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml.cs new file mode 100644 index 0000000..31885e7 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/BrainTypes.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 科学大脑类型分析 PageModel +/// +public class BrainTypesModel : ReportPageModelBase +{ + public BrainTypesModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml new file mode 100644 index 0000000..561139b --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml @@ -0,0 +1,19 @@ +@page "/report/character-types" +@model MiAssessment.Api.Pages.Report.CharacterTypesModel +@{ + ViewData["Title"] = "性格类型分析"; + ViewData["PageTitle"] = "性格类型分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

性格类型分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml.cs new file mode 100644 index 0000000..bdcee3c --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/CharacterTypes.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 性格类型分析 PageModel +/// +public class CharacterTypesModel : ReportPageModelBase +{ + public CharacterTypesModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml new file mode 100644 index 0000000..c7f22df --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml @@ -0,0 +1,24 @@ +@page "/report/cover" +@model MiAssessment.Api.Pages.Report.CoverModel +@{ + ViewData["Title"] = "封面页"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

多元智能测评报告

+
+

姓名:@Model.ReportData!.RecordInfo.Name

+

性别:@(Model.ReportData!.RecordInfo.Gender == 1 ? "男" : "女")

+

年龄:@Model.ReportData!.RecordInfo.Age

+

测评日期:@Model.ReportData!.RecordInfo.CompleteTime?.ToString("yyyy年MM月dd日")

+
+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml.cs new file mode 100644 index 0000000..7086dce --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/Cover.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 封面页 PageModel +/// +public class CoverModel : ReportPageModelBase +{ + public CoverModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml new file mode 100644 index 0000000..e9fcf4e --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml @@ -0,0 +1,19 @@ +@page "/report/future-abilities" +@model MiAssessment.Api.Pages.Report.FutureAbilitiesModel +@{ + ViewData["Title"] = "未来关键发展能力分析"; + ViewData["PageTitle"] = "未来关键发展能力分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

未来关键发展能力分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml.cs new file mode 100644 index 0000000..c607e5f --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/FutureAbilities.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 未来关键发展能力分析 PageModel +/// +public class FutureAbilitiesModel : ReportPageModelBase +{ + public FutureAbilitiesModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml new file mode 100644 index 0000000..25c5b47 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml @@ -0,0 +1,19 @@ +@page "/report/intelligence-overview" +@model MiAssessment.Api.Pages.Report.IntelligenceOverviewModel +@{ + ViewData["Title"] = "八大智能分析"; + ViewData["PageTitle"] = "八大智能分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

雷达图区域占位、柱状图区域占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml.cs new file mode 100644 index 0000000..1ba3ee3 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/IntelligenceOverview.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 八大智能分析 PageModel +/// +public class IntelligenceOverviewModel : ReportPageModelBase +{ + public IntelligenceOverviewModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml new file mode 100644 index 0000000..8149f7c --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml @@ -0,0 +1,19 @@ +@page "/report/learning-abilities" +@model MiAssessment.Api.Pages.Report.LearningAbilitiesModel +@{ + ViewData["Title"] = "学习关键能力分析"; + ViewData["PageTitle"] = "学习关键能力分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

学习关键能力分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml.cs new file mode 100644 index 0000000..8a57623 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningAbilities.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 学习关键能力分析 PageModel +/// +public class LearningAbilitiesModel : ReportPageModelBase +{ + public LearningAbilitiesModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml new file mode 100644 index 0000000..2fa7456 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml @@ -0,0 +1,19 @@ +@page "/report/learning-types" +@model MiAssessment.Api.Pages.Report.LearningTypesModel +@{ + ViewData["Title"] = "先天学习类型分析"; + ViewData["PageTitle"] = "先天学习类型分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

先天学习类型分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml.cs new file mode 100644 index 0000000..e0df603 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/LearningTypes.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 先天学习类型分析 PageModel +/// +public class LearningTypesModel : ReportPageModelBase +{ + public LearningTypesModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml new file mode 100644 index 0000000..113e722 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml @@ -0,0 +1,19 @@ +@page "/report/personality-traits" +@model MiAssessment.Api.Pages.Report.PersonalityTraitsModel +@{ + ViewData["Title"] = "个人特质分析"; + ViewData["PageTitle"] = "个人特质分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

个人特质分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml.cs new file mode 100644 index 0000000..74e4997 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/PersonalityTraits.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 个人特质分析 PageModel +/// +public class PersonalityTraitsModel : ReportPageModelBase +{ + public PersonalityTraitsModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/ReportPageModelBase.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/ReportPageModelBase.cs new file mode 100644 index 0000000..cd8d1d4 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/ReportPageModelBase.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using MiAssessment.Core.Interfaces; +using MiAssessment.Model.Models.Report; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 报告页面基类,提供公共的 recordId 解析和数据加载逻辑 +/// +public abstract class ReportPageModelBase : PageModel +{ + protected readonly IReportDataService ReportDataService; + + /// + /// 报告数据(所有页面共用) + /// + public ReportDataDto? ReportData { get; set; } + + /// + /// 错误信息(数据异常时设置) + /// + public string? ErrorMessage { get; set; } + + /// + /// 是否渲染成功 + /// + public bool IsSuccess => ErrorMessage == null && ReportData != null; + + protected ReportPageModelBase(IReportDataService reportDataService) + { + ReportDataService = reportDataService; + } + + /// + /// 加载报告数据,处理公共的参数验证和异常捕获 + /// + public async Task 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(); + } + } + + /// + /// 数据加载完成后的回调,子类可覆写 + /// + protected virtual Task OnDataLoadedAsync() => Task.CompletedTask; +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml new file mode 100644 index 0000000..a8427c2 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml @@ -0,0 +1,19 @@ +@page "/report/strongest-intelligence" +@model MiAssessment.Api.Pages.Report.StrongestIntelligenceModel +@{ + ViewData["Title"] = "最强智能详情"; + ViewData["PageTitle"] = "最强智能详情"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

最强智能详情占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml.cs new file mode 100644 index 0000000..0d73fbc --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/StrongestIntelligence.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 最强智能详情 PageModel +/// +public class StrongestIntelligenceModel : ReportPageModelBase +{ + public StrongestIntelligenceModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml new file mode 100644 index 0000000..28abd8a --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml @@ -0,0 +1,19 @@ +@page "/report/sub-abilities" +@model MiAssessment.Api.Pages.Report.SubAbilitiesModel +@{ + ViewData["Title"] = "40项细分能力分析"; + ViewData["PageTitle"] = "40项细分能力分析"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

40项细分能力分析占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml.cs new file mode 100644 index 0000000..83c8b0b --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/SubAbilities.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 40项细分能力分析 PageModel +/// +public class SubAbilitiesModel : ReportPageModelBase +{ + public SubAbilitiesModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml new file mode 100644 index 0000000..b6b0727 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml @@ -0,0 +1,19 @@ +@page "/report/weakest-intelligence" +@model MiAssessment.Api.Pages.Report.WeakestIntelligenceModel +@{ + ViewData["Title"] = "较弱智能详情"; + ViewData["PageTitle"] = "较弱智能详情"; +} + +@if (!Model.IsSuccess) +{ +
+

@Model.ErrorMessage

+
+} +else +{ +
+

较弱智能详情占位 - 具体内容后续实现

+
+} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml.cs b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml.cs new file mode 100644 index 0000000..f6d7c63 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/WeakestIntelligence.cshtml.cs @@ -0,0 +1,14 @@ +using MiAssessment.Core.Interfaces; + +namespace MiAssessment.Api.Pages.Report; + +/// +/// 较弱智能详情 PageModel +/// +public class WeakestIntelligenceModel : ReportPageModelBase +{ + public WeakestIntelligenceModel(IReportDataService reportDataService) + : base(reportDataService) + { + } +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ReportLayout.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ReportLayout.cshtml new file mode 100644 index 0000000..999ffe2 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ReportLayout.cshtml @@ -0,0 +1,45 @@ + + + + + + @ViewData["Title"] - 测评报告 + + @RenderSection("Styles", required: false) + + +
+ + @if (ViewData["PageTitle"] != null) + { +
+

@ViewData["PageTitle"]

+
+ } + + +
+ @RenderBody() +
+ + + @if (ViewData["PageNumber"] != null) + { + + } +
+ + @RenderSection("Scripts", required: false) + + + + + diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ViewStart.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ViewStart.cshtml new file mode 100644 index 0000000..3a286ba --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/Report/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_ReportLayout"; +} diff --git a/server/MiAssessment/src/MiAssessment.Api/Pages/_ViewImports.cshtml b/server/MiAssessment/src/MiAssessment.Api/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..1fa8de8 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using MiAssessment.Api +@namespace MiAssessment.Api.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/server/MiAssessment/src/MiAssessment.Api/Program.cs b/server/MiAssessment/src/MiAssessment.Api/Program.cs index a81dc6b..7b0c3c9 100644 --- a/server/MiAssessment/src/MiAssessment.Api/Program.cs +++ b/server/MiAssessment/src/MiAssessment.Api/Program.cs @@ -116,6 +116,9 @@ try // 注册报告生成队列消费者 builder.Services.AddHostedService(); + // 添加 Razor Pages 支持(报告网页渲染) + builder.Services.AddRazorPages(); + // 添加控制器 builder.Services.AddControllers(options => { @@ -185,6 +188,9 @@ try app.UseCors("Development"); } + // 使用静态文件(报告样式 report.css) + app.UseStaticFiles(); + // 使用 Serilog 请求日志 app.UseSerilogRequestLogging(); @@ -195,6 +201,9 @@ try app.UseAuthentication(); app.UseAuthorization(); + // 映射 Razor Pages(报告网页路由) + app.MapRazorPages(); + // 映射控制器 app.MapControllers(); diff --git a/server/MiAssessment/src/MiAssessment.Api/wwwroot/css/report.css b/server/MiAssessment/src/MiAssessment.Api/wwwroot/css/report.css new file mode 100644 index 0000000..808de9d --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Api/wwwroot/css/report.css @@ -0,0 +1,150 @@ +/* ============================================ + 测评报告公共样式 + 页面固定尺寸:1309×926px + ============================================ */ + +/* CSS 变量定义 */ +:root { + --primary-color: #4A90E2; + --primary-light: #6BA3E8; + --primary-dark: #3A7BC8; + --text-color: #333333; + --text-secondary: #666666; + --text-placeholder: #999999; + --text-disabled: #CCCCCC; + --border-color: #E8E8E8; + --border-light: #F0F0F0; + --bg-gray: #F8F8F8; + --bg-white: #FFFFFF; + --success-color: #52C41A; + --warning-color: #FAAD14; + --error-color: #FF4D4F; + --star-filled: #FAAD14; + --star-empty: #E8E8E8; +} + +/* 全局重置 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* ============================================ + 页面容器:固定 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; + font-size: 14px; + line-height: 1.6; + color: var(--text-color); + position: relative; + overflow: hidden; +} + +/* ============================================ + 页面头部 + ============================================ */ +.report-header { + margin-bottom: 20px; +} + +.report-title { + font-size: 28px; + font-weight: 600; + color: var(--text-color); + line-height: 1.3; +} + +/* ============================================ + 页面主体内容区 + ============================================ */ +.report-body { + flex: 1; + overflow: hidden; +} + +/* ============================================ + 页面底部页码 + ============================================ */ +.report-footer { + position: absolute; + bottom: 20px; + left: 0; + right: 0; + text-align: center; +} + +.page-number { + font-size: 12px; + color: var(--text-placeholder); +} + +/* ============================================ + 字体层级(按 1309×926px 页面尺寸设计) + ============================================ */ +h1 { + font-size: 28px; + font-weight: 600; + line-height: 1.3; +} + +h2 { + font-size: 22px; + font-weight: 600; + line-height: 1.4; +} + +h3 { + font-size: 18px; + font-weight: 600; + line-height: 1.4; +} + +/* 正文 14px,小字 12px 已在 .report-page 和 .page-number 中定义 */ + +/* ============================================ + 星级显示 + 使用 Unicode 星号:★ (filled) ☆ (empty) + ============================================ */ +.star-rating { + display: inline-flex; + align-items: center; + gap: 2px; + font-size: 16px; + line-height: 1; +} + +.star-filled { + color: var(--star-filled); +} + +.star-empty { + color: var(--star-empty); +} + +/* ============================================ + 错误页面 + ============================================ */ +.report-error { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--error-color); + font-size: 20px; +} + +/* ============================================ + 占位文本 + ============================================ */ +.no-content { + color: var(--text-placeholder); + font-style: italic; +} diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IReportDataService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IReportDataService.cs new file mode 100644 index 0000000..3fee548 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IReportDataService.cs @@ -0,0 +1,38 @@ +using MiAssessment.Model.Models.Report; + +namespace MiAssessment.Core.Interfaces; + +/// +/// 报告数据服务接口 +/// +/// +/// 提供报告渲染所需的数据查询功能,包括: +/// - 获取指定测评记录的完整报告数据 +/// - 确保结论数据存在(不存在则自动从模板生成) +/// +public interface IReportDataService +{ + /// + /// 获取指定测评记录的完整报告数据 + /// + /// + /// 从数据库查询测评记录、测评结果、记录结论和报告分类数据, + /// 组装为完整的 ReportDataDto 供 Razor Pages 渲染使用。 + /// 如果结论数据不存在,会自动触发生成。 + /// + /// 测评记录ID + /// 报告数据传输对象 + /// 记录不存在时抛出"测评记录不存在" + /// 记录状态不为4时抛出"报告尚未生成完成" + Task GetReportDataAsync(long recordId); + + /// + /// 确保指定测评记录的结论数据存在,不存在则自动生成 + /// + /// + /// 检查 assessment_record_conclusions 表中是否存在该记录的结论数据, + /// 如果不存在,则从 report_conclusions 模板表复制生成。 + /// + /// 测评记录ID + Task EnsureConclusionsExistAsync(long recordId); +} diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/ReportDataService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/ReportDataService.cs new file mode 100644 index 0000000..1eeb904 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Core/Services/ReportDataService.cs @@ -0,0 +1,274 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using MiAssessment.Core.Interfaces; +using MiAssessment.Model.Data; +using MiAssessment.Model.Entities; +using MiAssessment.Model.Models.Report; + +namespace MiAssessment.Core.Services; + +/// +/// 报告数据服务实现 +/// +/// +/// 提供报告渲染所需的数据查询功能,包括: +/// - 获取指定测评记录的完整报告数据 +/// - 确保结论数据存在(不存在则自动从模板生成) +/// +public class ReportDataService : IReportDataService +{ + private readonly MiAssessmentDbContext _dbContext; + private readonly ILogger _logger; + + public ReportDataService( + MiAssessmentDbContext dbContext, + ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + /// + public async Task GetReportDataAsync(long recordId) + { + // 1. 查询测评记录,验证存在且未软删除 + var record = await _dbContext.AssessmentRecords + .AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == recordId && !r.IsDeleted); + + if (record == null) + { + _logger.LogWarning("测评记录不存在,recordId: {RecordId}", recordId); + throw new InvalidOperationException("测评记录不存在"); + } + + // 2. 验证状态为4(已完成) + if (record.Status != 4) + { + _logger.LogWarning("报告尚未生成完成,recordId: {RecordId}, 当前状态: {Status}", recordId, record.Status); + throw new InvalidOperationException("报告尚未生成完成"); + } + + // 3. 确保结论数据存在 + await EnsureConclusionsExistAsync(recordId); + + // 4. 查询测评结果,关联报告分类获取 CategoryType + var results = await _dbContext.AssessmentResults + .AsNoTracking() + .Where(r => r.RecordId == recordId) + .Join( + _dbContext.ReportCategories.AsNoTracking().Where(c => !c.IsDeleted), + r => r.CategoryId, + c => c.Id, + (r, c) => new CategoryResultDataDto + { + CategoryId = r.CategoryId, + CategoryName = c.Name, + CategoryCode = c.Code, + CategoryType = c.CategoryType, + ParentId = c.ParentId, + Score = r.Score, + MaxScore = r.MaxScore, + Percentage = r.Percentage, + Rank = r.Rank, + StarLevel = r.StarLevel + }) + .ToListAsync(); + + // 5. 查询记录结论数据 + var conclusions = await _dbContext.AssessmentRecordConclusions + .AsNoTracking() + .Where(c => c.RecordId == recordId && !c.IsDeleted) + .Select(c => new ConclusionDataDto + { + CategoryId = c.CategoryId, + ConclusionType = c.ConclusionType, + StarLevel = c.StarLevel, + Title = c.Title, + Content = c.Content + }) + .ToListAsync(); + + // 6. 查询报告分类,构建分类树 + var categories = await _dbContext.ReportCategories + .AsNoTracking() + .Where(c => !c.IsDeleted) + .OrderBy(c => c.Sort) + .ToListAsync(); + + // 7. 组装 ReportDataDto + var reportData = new ReportDataDto + { + RecordInfo = new RecordInfoDto + { + RecordId = record.Id, + Name = record.Name, + Gender = record.Gender, + Age = record.Age, + EducationStage = record.EducationStage, + Province = record.Province, + City = record.City, + District = record.District, + CompleteTime = record.CompleteTime + }, + // 按 CategoryType 分组测评结果 + ResultsByType = results + .GroupBy(r => r.CategoryType) + .ToDictionary(g => g.Key, g => g.ToList()), + // 按 CategoryId 索引结论数据 + ConclusionsByCategory = conclusions + .ToDictionary(c => c.CategoryId, c => c), + // 构建分类树 + CategoryTrees = BuildCategoryTrees(categories) + }; + + _logger.LogDebug("报告数据加载完成,recordId: {RecordId}, 结果数: {ResultCount}, 结论数: {ConclusionCount}", + recordId, results.Count, conclusions.Count); + + return reportData; + } + + /// + public async Task EnsureConclusionsExistAsync(long recordId) + { + // 1. 检查是否已存在结论数据 + var hasConclusions = await _dbContext.AssessmentRecordConclusions + .AnyAsync(c => c.RecordId == recordId && !c.IsDeleted); + + if (hasConclusions) + { + _logger.LogDebug("结论数据已存在,跳过生成,recordId: {RecordId}", recordId); + return; + } + + _logger.LogInformation("结论数据不存在,开始自动生成,recordId: {RecordId}", recordId); + + // 2. 查询该记录的测评结果 + var assessmentResults = await _dbContext.AssessmentResults + .Where(r => r.RecordId == recordId) + .ToListAsync(); + + if (assessmentResults.Count == 0) + { + _logger.LogWarning("测评结果为空,无法生成结论,recordId: {RecordId}", recordId); + return; + } + + // 3. 根据星级确定结论类型,查询模板并复制 + var now = DateTime.Now; + var newConclusions = new List(); + + foreach (var result in assessmentResults) + { + var conclusionType = MapStarToConclusionType(result.StarLevel); + + // 从 report_conclusions 模板表查询对应结论 + var template = await _dbContext.ReportConclusions + .AsNoTracking() + .FirstOrDefaultAsync(t => + t.CategoryId == result.CategoryId && + t.ConclusionType == conclusionType && + !t.IsDeleted); + + if (template == null) + { + _logger.LogWarning("未找到结论模板,categoryId: {CategoryId}, conclusionType: {ConclusionType}", + result.CategoryId, conclusionType); + continue; + } + + // 复制模板数据到记录级别结论 + newConclusions.Add(new AssessmentRecordConclusion + { + RecordId = recordId, + CategoryId = result.CategoryId, + ConclusionType = conclusionType, + StarLevel = result.StarLevel, + Title = template.Title, + Content = template.Content, + CreateTime = now, + UpdateTime = now, + IsDeleted = false + }); + } + + if (newConclusions.Count > 0) + { + await _dbContext.AssessmentRecordConclusions.AddRangeAsync(newConclusions); + await _dbContext.SaveChangesAsync(); + _logger.LogInformation("结论数据生成完成,recordId: {RecordId}, 生成数量: {Count}", + recordId, newConclusions.Count); + } + } + + /// + /// 星级映射到结论类型 + /// + /// + /// 5星→1(最强), 4星→2(较强), 3星→2(较强), 2星→3(较弱), 1星→4(最弱) + /// + /// 星级(1-5) + /// 结论类型(1-4) + private static int MapStarToConclusionType(int starLevel) + { + return starLevel switch + { + 5 => 1, // 最强 + 4 => 2, // 较强 + 3 => 2, // 较强 + 2 => 3, // 较弱 + 1 => 4, // 最弱 + _ => 2 // 默认较强 + }; + } + + /// + /// 构建分类树结构 + /// + /// 所有分类列表 + /// 按 CategoryType 分组的分类树 + private static Dictionary> BuildCategoryTrees(List categories) + { + var result = new Dictionary>(); + + // 按 CategoryType 分组 + var grouped = categories.GroupBy(c => c.CategoryType); + + foreach (var group in grouped) + { + var categoryType = group.Key; + var items = group.ToList(); + + // 找出顶级分类(ParentId == 0) + var roots = items + .Where(c => c.ParentId == 0) + .Select(c => BuildTreeNode(c, items)) + .ToList(); + + result[categoryType] = roots; + } + + return result; + } + + /// + /// 递归构建分类树节点 + /// + private static CategoryTreeDto BuildTreeNode(ReportCategory category, List allCategories) + { + var node = new CategoryTreeDto + { + Id = category.Id, + Name = category.Name, + Code = category.Code, + CategoryType = category.CategoryType, + ParentId = category.ParentId, + Children = allCategories + .Where(c => c.ParentId == category.Id) + .Select(c => BuildTreeNode(c, allCategories)) + .ToList() + }; + + return node; + } +} diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs index 4598ba3..30e1a12 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs @@ -440,6 +440,8 @@ public class ReportGenerationService /// 事务内执行以下操作: /// - 删除该 RecordId 已有的 AssessmentResult 记录(支持重新生成) /// - 批量写入新的 AssessmentResult 记录 + /// - 删除该 RecordId 已有的 AssessmentRecordConclusion 记录(硬删除,支持重新生成) + /// - 从 report_conclusions 模板表复制结论数据到 assessment_record_conclusions /// - 更新 AssessmentRecord 的 Status 为 4(已完成)并设置 CompleteTime /// 异常时回滚事务,Status 保持为 3(生成中) /// @@ -478,6 +480,68 @@ public class ReportGenerationService await _dbContext.AssessmentResults.AddRangeAsync(newResults); + // 删除该 RecordId 已有的结论记录(硬删除,支持重新生成) + var existingConclusions = await _dbContext.AssessmentRecordConclusions + .Where(c => c.RecordId == recordId) + .ToListAsync(); + + if (existingConclusions.Count > 0) + { + _dbContext.AssessmentRecordConclusions.RemoveRange(existingConclusions); + _logger.LogDebug("删除已有结论记录,recordId: {RecordId}, 数量: {Count}", recordId, existingConclusions.Count); + } + + // 收集所有需要查询的 (CategoryId, ConclusionType) 组合 + var categoryConclusions = finalResults + .Select(r => new { r.CategoryId, r.ConclusionType }) + .ToList(); + + var categoryIds = categoryConclusions.Select(c => c.CategoryId).Distinct().ToList(); + var conclusionTypes = categoryConclusions.Select(c => c.ConclusionType).Distinct().ToList(); + + // 从 report_conclusions 模板表查询匹配的结论记录 + var templates = await _dbContext.ReportConclusions + .AsNoTracking() + .Where(c => !c.IsDeleted && categoryIds.Contains(c.CategoryId) && conclusionTypes.Contains(c.ConclusionType)) + .ToListAsync(); + + // 构建 (CategoryId, ConclusionType) → (Title, Content) 的快速查找字典 + var templateMap = templates + .GroupBy(c => (c.CategoryId, c.ConclusionType)) + .ToDictionary(g => g.Key, g => g.First()); + + // 复制结论数据到 assessment_record_conclusions + var newConclusions = new List(); + foreach (var result in finalResults) + { + if (templateMap.TryGetValue((result.CategoryId, result.ConclusionType), out var template)) + { + newConclusions.Add(new AssessmentRecordConclusion + { + RecordId = recordId, + CategoryId = result.CategoryId, + ConclusionType = result.ConclusionType, + StarLevel = result.StarLevel, + Title = template.Title, + Content = template.Content, + CreateTime = now, + UpdateTime = now, + IsDeleted = false + }); + } + else + { + _logger.LogWarning("未找到结论模板,recordId: {RecordId}, categoryId: {CategoryId}, conclusionType: {ConclusionType}", + recordId, result.CategoryId, result.ConclusionType); + } + } + + if (newConclusions.Count > 0) + { + await _dbContext.AssessmentRecordConclusions.AddRangeAsync(newConclusions); + _logger.LogDebug("复制结论数据,recordId: {RecordId}, 数量: {Count}", recordId, newConclusions.Count); + } + // 重新加载测评记录(带跟踪),更新状态为4(已完成) var record = await _dbContext.AssessmentRecords .FirstAsync(r => r.Id == recordId); @@ -489,7 +553,8 @@ public class ReportGenerationService await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); - _logger.LogInformation("结果持久化完成,recordId: {RecordId}, 写入记录数: {Count}", recordId, newResults.Count); + _logger.LogInformation("结果持久化完成,recordId: {RecordId}, 写入结果数: {ResultCount}, 写入结论数: {ConclusionCount}", + recordId, newResults.Count, newConclusions.Count); } catch (Exception ex) { diff --git a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs index 7e85f4e..e2ed130 100644 --- a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs +++ b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs @@ -134,6 +134,16 @@ public class ServiceModule : Module // 注册报告队列生产者 builder.RegisterType().As().InstancePerLifetimeScope(); + // ========== 报告数据服务注册 ========== + + // 注册报告数据服务(供 Razor Pages 报告渲染使用) + builder.Register(c => + { + var dbContext = c.Resolve(); + var logger = c.Resolve>(); + return new ReportDataService(dbContext, logger); + }).As().InstancePerLifetimeScope(); + // ========== 小程序测评模块服务注册 ========== // 注册报告生成服务 diff --git a/server/MiAssessment/src/MiAssessment.Model/Data/MiAssessmentDbContext.cs b/server/MiAssessment/src/MiAssessment.Model/Data/MiAssessmentDbContext.cs index 90fadf2..0fedcd1 100644 --- a/server/MiAssessment/src/MiAssessment.Model/Data/MiAssessmentDbContext.cs +++ b/server/MiAssessment/src/MiAssessment.Model/Data/MiAssessmentDbContext.cs @@ -67,6 +67,11 @@ public partial class MiAssessmentDbContext : DbContext public virtual DbSet HomeNavigations { get; set; } + // ==================== 报告网页版相关表 ==================== + public virtual DbSet AssessmentRecordConclusions { get; set; } + + public virtual DbSet ReportPageConfigs { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Connection string is configured in Program.cs via dependency injection @@ -1052,6 +1057,81 @@ public partial class MiAssessmentDbContext : DbContext .HasComment("软删除标记"); }); + // ==================== 测评记录结论表配置 ==================== + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk_assessment_record_conclusions"); + + entity.ToTable("assessment_record_conclusions", tb => tb.HasComment("测评记录结论表,存储每条测评记录的独立结论数据副本")); + + entity.HasIndex(e => e.RecordId).HasDatabaseName("ix_arc_record_id"); + entity.HasIndex(e => new { e.RecordId, e.CategoryId }).HasDatabaseName("ix_arc_record_category"); + + entity.Property(e => e.Id) + .HasComment("主键ID"); + entity.Property(e => e.RecordId) + .HasComment("测评记录ID"); + entity.Property(e => e.CategoryId) + .HasComment("分类ID"); + entity.Property(e => e.ConclusionType) + .HasComment("结论类型:1最强 2较强 3较弱 4最弱"); + entity.Property(e => e.StarLevel) + .HasComment("星级(1-5),记录级别的星级,可由管理员覆盖"); + entity.Property(e => e.Title) + .HasMaxLength(100) + .HasComment("结论标题"); + entity.Property(e => e.Content) + .HasComment("结论内容(富文本)"); + entity.Property(e => e.CreateTime) + .HasDefaultValueSql("(getdate())") + .HasComment("创建时间"); + entity.Property(e => e.UpdateTime) + .HasDefaultValueSql("(getdate())") + .HasComment("更新时间"); + entity.Property(e => e.IsDeleted) + .HasDefaultValue(false) + .HasComment("软删除标记"); + }); + + // ==================== 报告页面配置表配置 ==================== + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk_report_page_configs"); + + entity.ToTable("report_page_configs", tb => tb.HasComment("报告页面配置表,定义PDF报告中每一页的类型和顺序")); + + entity.HasIndex(e => e.SortOrder).HasDatabaseName("ix_rpc_sort_order"); + entity.HasIndex(e => e.Status).HasDatabaseName("ix_rpc_status"); + + entity.Property(e => e.Id) + .HasComment("主键ID"); + entity.Property(e => e.PageType) + .HasComment("页面类型:1静态图片 2网页截图"); + entity.Property(e => e.PageName) + .HasMaxLength(50) + .HasComment("页面标识名称"); + entity.Property(e => e.Title) + .HasMaxLength(100) + .HasComment("页面显示标题"); + entity.Property(e => e.SortOrder) + .HasComment("排序序号"); + entity.Property(e => e.ImageUrl) + .HasMaxLength(500) + .HasComment("静态图片路径(PageType=1时使用)"); + entity.Property(e => e.RouteUrl) + .HasMaxLength(200) + .HasComment("网页路由路径(PageType=2时使用)"); + entity.Property(e => e.Status) + .HasDefaultValue(1) + .HasComment("状态:0禁用 1启用"); + entity.Property(e => e.CreateTime) + .HasDefaultValueSql("(getdate())") + .HasComment("创建时间"); + entity.Property(e => e.UpdateTime) + .HasDefaultValueSql("(getdate())") + .HasComment("更新时间"); + }); + OnModelCreatingPartial(modelBuilder); } diff --git a/server/MiAssessment/src/MiAssessment.Model/Entities/AssessmentRecordConclusion.cs b/server/MiAssessment/src/MiAssessment.Model/Entities/AssessmentRecordConclusion.cs new file mode 100644 index 0000000..959bfcf --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Model/Entities/AssessmentRecordConclusion.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MiAssessment.Model.Entities; + +/// +/// 测评记录结论表 +/// +[Table("assessment_record_conclusions")] +public class AssessmentRecordConclusion +{ + /// + /// 主键ID + /// + [Key] + public long Id { get; set; } + + /// + /// 测评记录ID + /// + public long RecordId { get; set; } + + /// + /// 分类ID + /// + public long CategoryId { get; set; } + + /// + /// 结论类型:1最强 2较强 3较弱 4最弱 + /// + public int ConclusionType { get; set; } + + /// + /// 星级(1-5),记录级别的星级,可由管理员覆盖 + /// + 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; } +} diff --git a/server/MiAssessment/src/MiAssessment.Model/Entities/ReportPageConfig.cs b/server/MiAssessment/src/MiAssessment.Model/Entities/ReportPageConfig.cs new file mode 100644 index 0000000..3c8a58b --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Model/Entities/ReportPageConfig.cs @@ -0,0 +1,69 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MiAssessment.Model.Entities; + +/// +/// 报告页面配置表 +/// +[Table("report_page_configs")] +public class ReportPageConfig +{ + /// + /// 主键ID + /// + [Key] + public long Id { get; set; } + + /// + /// 页面类型:1静态图片 2网页截图 + /// + public int PageType { get; set; } + + /// + /// 页面标识名称 + /// + [Required] + [MaxLength(50)] + public string PageName { get; set; } = null!; + + /// + /// 页面显示标题 + /// + [Required] + [MaxLength(100)] + public string Title { get; set; } = null!; + + /// + /// 排序序号 + /// + public int SortOrder { get; set; } + + /// + /// 静态图片路径(PageType=1 时使用) + /// + [MaxLength(500)] + public string? ImageUrl { get; set; } + + /// + /// 网页路由路径(PageType=2 时使用) + /// + [MaxLength(200)] + public string? RouteUrl { get; set; } + + /// + /// 状态:0禁用 1启用 + /// + public int Status { get; set; } = 1; + + /// + /// 创建时间 + /// + public DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime UpdateTime { get; set; } +} diff --git a/server/MiAssessment/src/MiAssessment.Model/Models/Report/ReportDataDto.cs b/server/MiAssessment/src/MiAssessment.Model/Models/Report/ReportDataDto.cs new file mode 100644 index 0000000..140cf36 --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Model/Models/Report/ReportDataDto.cs @@ -0,0 +1,204 @@ +namespace MiAssessment.Model.Models.Report; + +/// +/// 报告完整数据 DTO +/// +public class ReportDataDto +{ + /// + /// 测评记录基本信息 + /// + public RecordInfoDto RecordInfo { get; set; } = null!; + + /// + /// 按 CategoryType 分组的测评结果 + /// Key: CategoryType (1-8), Value: 该类型下所有分类的结果列表 + /// + public Dictionary> ResultsByType { get; set; } = new(); + + /// + /// 按 CategoryId 索引的结论数据 + /// Key: CategoryId, Value: 该分类的结论数据 + /// + public Dictionary ConclusionsByCategory { get; set; } = new(); + + /// + /// 报告分类层级结构 + /// Key: CategoryType, Value: 该类型的分类树 + /// + public Dictionary> CategoryTrees { get; set; } = new(); +} + +/// +/// 测评记录基本信息 +/// +public class RecordInfoDto +{ + /// + /// 测评记录ID + /// + public long RecordId { get; set; } + + /// + /// 测评人姓名 + /// + public string Name { get; set; } = null!; + + /// + /// 性别(1男 2女) + /// + 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; } +} + +/// +/// 分类测评结果数据 +/// +public class CategoryResultDataDto +{ + /// + /// 分类ID + /// + public long CategoryId { get; set; } + + /// + /// 分类名称 + /// + public string CategoryName { get; set; } = null!; + + /// + /// 分类编码 + /// + public string CategoryCode { get; set; } = null!; + + /// + /// 分类类型(1-8) + /// + public int CategoryType { get; set; } + + /// + /// 父分类ID + /// + 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; } + + /// + /// 星级(1-5) + /// + public int StarLevel { get; set; } +} + +/// +/// 结论数据 +/// +public class ConclusionDataDto +{ + /// + /// 分类ID + /// + public long CategoryId { get; set; } + + /// + /// 结论类型:1最强 2较强 3较弱 4最弱 + /// + public int ConclusionType { get; set; } + + /// + /// 星级(1-5) + /// + public int StarLevel { get; set; } + + /// + /// 结论标题 + /// + public string? Title { get; set; } + + /// + /// 结论内容(富文本) + /// + public string Content { get; set; } = null!; +} + +/// +/// 分类树节点 +/// +public class CategoryTreeDto +{ + /// + /// 分类ID + /// + public long Id { get; set; } + + /// + /// 分类名称 + /// + public string Name { get; set; } = null!; + + /// + /// 分类编码 + /// + public string Code { get; set; } = null!; + + /// + /// 分类类型(1-8) + /// + public int CategoryType { get; set; } + + /// + /// 父分类ID + /// + public long ParentId { get; set; } + + /// + /// 子分类列表 + /// + public List Children { get; set; } = new(); +} diff --git a/temp_sql/batch18_report_web_pages.sql b/temp_sql/batch18_report_web_pages.sql new file mode 100644 index 0000000..d894379 --- /dev/null +++ b/temp_sql/batch18_report_web_pages.sql @@ -0,0 +1,144 @@ +-- ============================================================ +-- 创建 assessment_record_conclusions 表 +-- 存储每条测评记录的独立结论数据副本 +-- 由报告生成时从 report_conclusions 模板表复制而来 +-- Database: MiAssessment_Business (SQL Server 2022) +-- ============================================================ + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'assessment_record_conclusions') +BEGIN + CREATE TABLE assessment_record_conclusions ( + Id bigint IDENTITY(1,1) NOT NULL, + RecordId bigint NOT NULL, + CategoryId bigint NOT NULL, + ConclusionType int NOT NULL, + StarLevel int NOT NULL, + Title nvarchar(100) NULL, + Content nvarchar(max) NOT NULL, + CreateTime datetime NOT NULL CONSTRAINT DF_arc_CreateTime DEFAULT (GETDATE()), + UpdateTime datetime NOT NULL CONSTRAINT DF_arc_UpdateTime DEFAULT (GETDATE()), + IsDeleted bit NOT NULL CONSTRAINT DF_arc_IsDeleted DEFAULT (0), + + CONSTRAINT PK_assessment_record_conclusions PRIMARY KEY CLUSTERED (Id), + CONSTRAINT FK_arc_assessment_records FOREIGN KEY (RecordId) REFERENCES assessment_records (Id), + CONSTRAINT FK_arc_report_categories FOREIGN KEY (CategoryId) REFERENCES report_categories (Id) + ); + + -- 按测评记录查询 + CREATE NONCLUSTERED INDEX ix_arc_record_id + ON assessment_record_conclusions (RecordId); + + -- 按记录+分类查询 + CREATE NONCLUSTERED INDEX ix_arc_record_category + ON assessment_record_conclusions (RecordId, CategoryId); + + PRINT N'表 assessment_record_conclusions 创建成功'; +END +ELSE +BEGIN + PRINT N'表 assessment_record_conclusions 已存在,跳过创建'; +END +GO + +-- ============================================================ +-- 创建 report_page_configs 表 +-- 定义 PDF 报告中每一页的类型、顺序和关联资源 +-- Database: MiAssessment_Business (SQL Server 2022) +-- ============================================================ + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'report_page_configs') +BEGIN + CREATE TABLE report_page_configs ( + Id bigint IDENTITY(1,1) NOT NULL, + PageType int NOT NULL, + PageName nvarchar(50) NOT NULL, + Title nvarchar(100) NOT NULL, + SortOrder int NOT NULL, + ImageUrl nvarchar(500) NULL, + RouteUrl nvarchar(200) NULL, + Status int NOT NULL CONSTRAINT DF_rpc_Status DEFAULT (1), + CreateTime datetime NOT NULL CONSTRAINT DF_rpc_CreateTime DEFAULT (GETDATE()), + UpdateTime datetime NOT NULL CONSTRAINT DF_rpc_UpdateTime DEFAULT (GETDATE()), + + CONSTRAINT PK_report_page_configs PRIMARY KEY CLUSTERED (Id) + ); + + -- 按排序查询 + CREATE NONCLUSTERED INDEX ix_rpc_sort_order + ON report_page_configs (SortOrder); + + -- 按状态筛选 + CREATE NONCLUSTERED INDEX ix_rpc_status + ON report_page_configs (Status); + + PRINT N'表 report_page_configs 创建成功'; +END +ELSE +BEGIN + PRINT N'表 report_page_configs 已存在,跳过创建'; +END +GO + +-- ============================================================ +-- 插入 report_page_configs 初始数据 +-- 包含静态图片页(PageType=1)和网页截图页(PageType=2) +-- 按 SortOrder 排序 +-- ============================================================ + +-- 仅在表为空时插入初始数据 +IF NOT EXISTS (SELECT 1 FROM report_page_configs) +BEGIN + INSERT INTO report_page_configs (PageType, PageName, Title, SortOrder, ImageUrl, RouteUrl, Status) + VALUES + -- 静态图片页:报告封面图 + (1, N'cover-image', N'报告封面图', 1, N'/images/report/cover.png', NULL, 1), + + -- 网页截图页:封面页(测评人信息) + (2, N'cover', N'封面页', 2, NULL, N'/report/cover'), + + -- 静态图片页:目录/导读页 + (1, N'intro-page', N'目录导读页', 3, N'/images/report/intro.png', NULL, 1), + + -- 网页截图页:八大智能分析 + (2, N'intelligence-overview', N'八大智能分析', 4, NULL, N'/report/intelligence-overview'), + + -- 网页截图页:最强智能详情 + (2, N'strongest-intelligence', N'最强智能详情', 5, NULL, N'/report/strongest-intelligence'), + + -- 网页截图页:较弱智能详情 + (2, N'weakest-intelligence', N'较弱智能详情', 6, NULL, N'/report/weakest-intelligence'), + + -- 网页截图页:个人特质分析 + (2, N'personality-traits', N'个人特质分析', 7, NULL, N'/report/personality-traits'), + + -- 网页截图页:40项细分能力分析 + (2, N'sub-abilities', N'40项细分能力分析', 8, NULL, N'/report/sub-abilities'), + + -- 静态图片页:板块分隔页 + (1, N'separator-learning', N'学习分析分隔页', 9, N'/images/report/separator-learning.png', NULL, 1), + + -- 网页截图页:先天学习类型分析 + (2, N'learning-types', N'先天学习类型分析', 10, NULL, N'/report/learning-types'), + + -- 网页截图页:学习关键能力分析 + (2, N'learning-abilities', N'学习关键能力分析', 11, NULL, N'/report/learning-abilities'), + + -- 网页截图页:科学大脑类型分析 + (2, N'brain-types', N'科学大脑类型分析', 12, NULL, N'/report/brain-types'), + + -- 网页截图页:性格类型分析 + (2, N'character-types', N'性格类型分析', 13, NULL, N'/report/character-types'), + + -- 网页截图页:未来关键发展能力分析 + (2, N'future-abilities', N'未来关键发展能力分析', 14, NULL, N'/report/future-abilities'), + + -- 静态图片页:报告尾页 + (1, N'ending-page', N'报告尾页', 15, N'/images/report/ending.png', NULL, 1); + + PRINT N'report_page_configs 初始数据插入成功(共 15 条)'; +END +ELSE +BEGIN + PRINT N'report_page_configs 已有数据,跳过初始数据插入'; +END +GO