21
This commit is contained in:
parent
09be2bec81
commit
d2a4f01e50
733
.kiro/specs/report-web-pages/design.md
Normal file
733
.kiro/specs/report-web-pages/design.md
Normal file
|
|
@ -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 `<script>` 标签引入,避免增加后端项目的静态资源体积
|
||||
- **结论数据独立副本**:每条测评记录拥有独立的结论数据,管理员调整不影响模板和其他记录,支持"重新生成"恢复到模板数据
|
||||
- **新增实体放在 MiAssessment.Model**:`AssessmentRecordConclusion` 和 `ReportPageConfig` 实体在 Model 项目中定义,供 Api 和 Admin.Business 共用
|
||||
- **先搭框架后做页面**:本设计聚焦框架层面(数据表、服务、路由、公共模板),具体每个页面的样式布局后续逐个实现
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体数据流
|
||||
|
||||
```mermaid
|
||||
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 服务。
|
||||
|
||||
```csharp
|
||||
/// <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` 方法的事务中,增加结论数据复制逻辑:
|
||||
|
||||
```csharp
|
||||
// 在写入 AssessmentResult 之后,复制结论数据
|
||||
// 1. 删除该 RecordId 已有的 AssessmentRecordConclusion 记录
|
||||
// 2. 根据每个 FinalCategoryResult 的 CategoryId 和 ConclusionType
|
||||
// 从 report_conclusions 查询模板,复制 Title 和 Content 到 assessment_record_conclusions
|
||||
```
|
||||
|
||||
### 3. Razor Pages PageModel 基类
|
||||
|
||||
```csharp
|
||||
/// <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`
|
||||
|
||||
```html
|
||||
<!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` 核心规则
|
||||
|
||||
```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; }
|
||||
|
||||
/// <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_order` ON (SortOrder) — 按排序查询
|
||||
- `ix_rpc_status` ON (Status) — 按状态筛选
|
||||
|
||||
对应实体类 `ReportPageConfig`:
|
||||
|
||||
```csharp
|
||||
[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(报告数据传输对象)
|
||||
|
||||
```csharp
|
||||
/// <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 集成变更
|
||||
|
||||
```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<Program>` 进行端到端测试:
|
||||
|
||||
1. 启动测试服务器,访问各报告页面路由
|
||||
2. 验证 HTTP 200 响应和 HTML 内容
|
||||
3. 验证错误场景的 HTML 输出
|
||||
4. 验证 data-render-complete / data-render-error 属性
|
||||
237
.kiro/specs/report-web-pages/tasks.md
Normal file
237
.kiro/specs/report-web-pages/tasks.md
Normal file
|
|
@ -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<AssessmentRecordConclusion>` 和 `DbSet<ReportPageConfig>`
|
||||
- 在 `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 个报告页面的具体样式和内容实现将在后续逐个开发
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/brain-types"
|
||||
@model MiAssessment.Api.Pages.Report.BrainTypesModel
|
||||
@{
|
||||
ViewData["Title"] = "科学大脑类型分析";
|
||||
ViewData["PageTitle"] = "科学大脑类型分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="brain-types-content">
|
||||
<p class="no-content">科学大脑类型分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 科学大脑类型分析 PageModel
|
||||
/// </summary>
|
||||
public class BrainTypesModel : ReportPageModelBase
|
||||
{
|
||||
public BrainTypesModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/character-types"
|
||||
@model MiAssessment.Api.Pages.Report.CharacterTypesModel
|
||||
@{
|
||||
ViewData["Title"] = "性格类型分析";
|
||||
ViewData["PageTitle"] = "性格类型分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="character-types-content">
|
||||
<p class="no-content">性格类型分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 性格类型分析 PageModel
|
||||
/// </summary>
|
||||
public class CharacterTypesModel : ReportPageModelBase
|
||||
{
|
||||
public CharacterTypesModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@page "/report/cover"
|
||||
@model MiAssessment.Api.Pages.Report.CoverModel
|
||||
@{
|
||||
ViewData["Title"] = "封面页";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="cover-content">
|
||||
<h1>多元智能测评报告</h1>
|
||||
<div class="cover-info">
|
||||
<p>姓名:@Model.ReportData!.RecordInfo.Name</p>
|
||||
<p>性别:@(Model.ReportData!.RecordInfo.Gender == 1 ? "男" : "女")</p>
|
||||
<p>年龄:@Model.ReportData!.RecordInfo.Age</p>
|
||||
<p>测评日期:@Model.ReportData!.RecordInfo.CompleteTime?.ToString("yyyy年MM月dd日")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 封面页 PageModel
|
||||
/// </summary>
|
||||
public class CoverModel : ReportPageModelBase
|
||||
{
|
||||
public CoverModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/future-abilities"
|
||||
@model MiAssessment.Api.Pages.Report.FutureAbilitiesModel
|
||||
@{
|
||||
ViewData["Title"] = "未来关键发展能力分析";
|
||||
ViewData["PageTitle"] = "未来关键发展能力分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="future-abilities-content">
|
||||
<p class="no-content">未来关键发展能力分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 未来关键发展能力分析 PageModel
|
||||
/// </summary>
|
||||
public class FutureAbilitiesModel : ReportPageModelBase
|
||||
{
|
||||
public FutureAbilitiesModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/intelligence-overview"
|
||||
@model MiAssessment.Api.Pages.Report.IntelligenceOverviewModel
|
||||
@{
|
||||
ViewData["Title"] = "八大智能分析";
|
||||
ViewData["PageTitle"] = "八大智能分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="intelligence-overview-content">
|
||||
<p class="no-content">雷达图区域占位、柱状图区域占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 八大智能分析 PageModel
|
||||
/// </summary>
|
||||
public class IntelligenceOverviewModel : ReportPageModelBase
|
||||
{
|
||||
public IntelligenceOverviewModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/learning-abilities"
|
||||
@model MiAssessment.Api.Pages.Report.LearningAbilitiesModel
|
||||
@{
|
||||
ViewData["Title"] = "学习关键能力分析";
|
||||
ViewData["PageTitle"] = "学习关键能力分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="learning-abilities-content">
|
||||
<p class="no-content">学习关键能力分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 学习关键能力分析 PageModel
|
||||
/// </summary>
|
||||
public class LearningAbilitiesModel : ReportPageModelBase
|
||||
{
|
||||
public LearningAbilitiesModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/learning-types"
|
||||
@model MiAssessment.Api.Pages.Report.LearningTypesModel
|
||||
@{
|
||||
ViewData["Title"] = "先天学习类型分析";
|
||||
ViewData["PageTitle"] = "先天学习类型分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="learning-types-content">
|
||||
<p class="no-content">先天学习类型分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 先天学习类型分析 PageModel
|
||||
/// </summary>
|
||||
public class LearningTypesModel : ReportPageModelBase
|
||||
{
|
||||
public LearningTypesModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/personality-traits"
|
||||
@model MiAssessment.Api.Pages.Report.PersonalityTraitsModel
|
||||
@{
|
||||
ViewData["Title"] = "个人特质分析";
|
||||
ViewData["PageTitle"] = "个人特质分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="personality-traits-content">
|
||||
<p class="no-content">个人特质分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 个人特质分析 PageModel
|
||||
/// </summary>
|
||||
public class PersonalityTraitsModel : ReportPageModelBase
|
||||
{
|
||||
public PersonalityTraitsModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/strongest-intelligence"
|
||||
@model MiAssessment.Api.Pages.Report.StrongestIntelligenceModel
|
||||
@{
|
||||
ViewData["Title"] = "最强智能详情";
|
||||
ViewData["PageTitle"] = "最强智能详情";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="strongest-intelligence-content">
|
||||
<p class="no-content">最强智能详情占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 最强智能详情 PageModel
|
||||
/// </summary>
|
||||
public class StrongestIntelligenceModel : ReportPageModelBase
|
||||
{
|
||||
public StrongestIntelligenceModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/sub-abilities"
|
||||
@model MiAssessment.Api.Pages.Report.SubAbilitiesModel
|
||||
@{
|
||||
ViewData["Title"] = "40项细分能力分析";
|
||||
ViewData["PageTitle"] = "40项细分能力分析";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="sub-abilities-content">
|
||||
<p class="no-content">40项细分能力分析占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 40项细分能力分析 PageModel
|
||||
/// </summary>
|
||||
public class SubAbilitiesModel : ReportPageModelBase
|
||||
{
|
||||
public SubAbilitiesModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@page "/report/weakest-intelligence"
|
||||
@model MiAssessment.Api.Pages.Report.WeakestIntelligenceModel
|
||||
@{
|
||||
ViewData["Title"] = "较弱智能详情";
|
||||
ViewData["PageTitle"] = "较弱智能详情";
|
||||
}
|
||||
|
||||
@if (!Model.IsSuccess)
|
||||
{
|
||||
<div class="report-error" data-render-error="true">
|
||||
<p>@Model.ErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="weakest-intelligence-content">
|
||||
<p class="no-content">较弱智能详情占位 - 具体内容后续实现</p>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MiAssessment.Core.Interfaces;
|
||||
|
||||
namespace MiAssessment.Api.Pages.Report;
|
||||
|
||||
/// <summary>
|
||||
/// 较弱智能详情 PageModel
|
||||
/// </summary>
|
||||
public class WeakestIntelligenceModel : ReportPageModelBase
|
||||
{
|
||||
public WeakestIntelligenceModel(IReportDataService reportDataService)
|
||||
: base(reportDataService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@{
|
||||
Layout = "_ReportLayout";
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@using MiAssessment.Api
|
||||
@namespace MiAssessment.Api.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
|
@ -116,6 +116,9 @@ try
|
|||
// 注册报告生成队列消费者
|
||||
builder.Services.AddHostedService<ReportQueueConsumer>();
|
||||
|
||||
// 添加 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();
|
||||
|
||||
|
|
|
|||
150
server/MiAssessment/src/MiAssessment.Api/wwwroot/css/report.css
Normal file
150
server/MiAssessment/src/MiAssessment.Api/wwwroot/css/report.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using MiAssessment.Model.Models.Report;
|
||||
|
||||
namespace MiAssessment.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 报告数据服务接口
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 提供报告渲染所需的数据查询功能,包括:
|
||||
/// - 获取指定测评记录的完整报告数据
|
||||
/// - 确保结论数据存在(不存在则自动从模板生成)
|
||||
/// </remarks>
|
||||
public interface IReportDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取指定测评记录的完整报告数据
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 从数据库查询测评记录、测评结果、记录结论和报告分类数据,
|
||||
/// 组装为完整的 ReportDataDto 供 Razor Pages 渲染使用。
|
||||
/// 如果结论数据不存在,会自动触发生成。
|
||||
/// </remarks>
|
||||
/// <param name="recordId">测评记录ID</param>
|
||||
/// <returns>报告数据传输对象</returns>
|
||||
/// <exception cref="Exception">记录不存在时抛出"测评记录不存在"</exception>
|
||||
/// <exception cref="Exception">记录状态不为4时抛出"报告尚未生成完成"</exception>
|
||||
Task<ReportDataDto> GetReportDataAsync(long recordId);
|
||||
|
||||
/// <summary>
|
||||
/// 确保指定测评记录的结论数据存在,不存在则自动生成
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 检查 assessment_record_conclusions 表中是否存在该记录的结论数据,
|
||||
/// 如果不存在,则从 report_conclusions 模板表复制生成。
|
||||
/// </remarks>
|
||||
/// <param name="recordId">测评记录ID</param>
|
||||
Task EnsureConclusionsExistAsync(long recordId);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 报告数据服务实现
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 提供报告渲染所需的数据查询功能,包括:
|
||||
/// - 获取指定测评记录的完整报告数据
|
||||
/// - 确保结论数据存在(不存在则自动从模板生成)
|
||||
/// </remarks>
|
||||
public class ReportDataService : IReportDataService
|
||||
{
|
||||
private readonly MiAssessmentDbContext _dbContext;
|
||||
private readonly ILogger<ReportDataService> _logger;
|
||||
|
||||
public ReportDataService(
|
||||
MiAssessmentDbContext dbContext,
|
||||
ILogger<ReportDataService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ReportDataDto> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<AssessmentRecordConclusion>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 星级映射到结论类型
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 5星→1(最强), 4星→2(较强), 3星→2(较强), 2星→3(较弱), 1星→4(最弱)
|
||||
/// </remarks>
|
||||
/// <param name="starLevel">星级(1-5)</param>
|
||||
/// <returns>结论类型(1-4)</returns>
|
||||
private static int MapStarToConclusionType(int starLevel)
|
||||
{
|
||||
return starLevel switch
|
||||
{
|
||||
5 => 1, // 最强
|
||||
4 => 2, // 较强
|
||||
3 => 2, // 较强
|
||||
2 => 3, // 较弱
|
||||
1 => 4, // 最弱
|
||||
_ => 2 // 默认较强
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建分类树结构
|
||||
/// </summary>
|
||||
/// <param name="categories">所有分类列表</param>
|
||||
/// <returns>按 CategoryType 分组的分类树</returns>
|
||||
private static Dictionary<int, List<CategoryTreeDto>> BuildCategoryTrees(List<ReportCategory> categories)
|
||||
{
|
||||
var result = new Dictionary<int, List<CategoryTreeDto>>();
|
||||
|
||||
// 按 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归构建分类树节点
|
||||
/// </summary>
|
||||
private static CategoryTreeDto BuildTreeNode(ReportCategory category, List<ReportCategory> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -440,6 +440,8 @@ public class ReportGenerationService
|
|||
/// 事务内执行以下操作:
|
||||
/// - 删除该 RecordId 已有的 AssessmentResult 记录(支持重新生成)
|
||||
/// - 批量写入新的 AssessmentResult 记录
|
||||
/// - 删除该 RecordId 已有的 AssessmentRecordConclusion 记录(硬删除,支持重新生成)
|
||||
/// - 从 report_conclusions 模板表复制结论数据到 assessment_record_conclusions
|
||||
/// - 更新 AssessmentRecord 的 Status 为 4(已完成)并设置 CompleteTime
|
||||
/// 异常时回滚事务,Status 保持为 3(生成中)
|
||||
/// </remarks>
|
||||
|
|
@ -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<AssessmentRecordConclusion>();
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -134,6 +134,16 @@ public class ServiceModule : Module
|
|||
// 注册报告队列生产者
|
||||
builder.RegisterType<ReportQueueProducer>().As<IReportQueueProducer>().InstancePerLifetimeScope();
|
||||
|
||||
// ========== 报告数据服务注册 ==========
|
||||
|
||||
// 注册报告数据服务(供 Razor Pages 报告渲染使用)
|
||||
builder.Register(c =>
|
||||
{
|
||||
var dbContext = c.Resolve<MiAssessmentDbContext>();
|
||||
var logger = c.Resolve<ILogger<ReportDataService>>();
|
||||
return new ReportDataService(dbContext, logger);
|
||||
}).As<IReportDataService>().InstancePerLifetimeScope();
|
||||
|
||||
// ========== 小程序测评模块服务注册 ==========
|
||||
|
||||
// 注册报告生成服务
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ public partial class MiAssessmentDbContext : DbContext
|
|||
|
||||
public virtual DbSet<HomeNavigation> HomeNavigations { get; set; }
|
||||
|
||||
// ==================== 报告网页版相关表 ====================
|
||||
public virtual DbSet<AssessmentRecordConclusion> AssessmentRecordConclusions { get; set; }
|
||||
|
||||
public virtual DbSet<ReportPageConfig> 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<AssessmentRecordConclusion>(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<ReportPageConfig>(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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace MiAssessment.Model.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 测评记录结论表
|
||||
/// </summary>
|
||||
[Table("assessment_record_conclusions")]
|
||||
public class AssessmentRecordConclusion
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
[Key]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 测评记录ID
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类ID
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// 结论标题
|
||||
/// </summary>
|
||||
[MaxLength(100)]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结论内容(富文本)
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Column(TypeName = "nvarchar(max)")]
|
||||
public string Content { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 软删除标记
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的测评记录
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(RecordId))]
|
||||
public virtual AssessmentRecord? Record { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的报告分类
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(CategoryId))]
|
||||
public virtual ReportCategory? Category { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace MiAssessment.Model.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 报告页面配置表
|
||||
/// </summary>
|
||||
[Table("report_page_configs")]
|
||||
public class ReportPageConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdateTime { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
namespace MiAssessment.Model.Models.Report;
|
||||
|
||||
/// <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
|
||||
{
|
||||
/// <summary>
|
||||
/// 测评记录ID
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 测评人姓名
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 性别(1男 2女)
|
||||
/// </summary>
|
||||
public int Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 年龄
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 学业阶段
|
||||
/// </summary>
|
||||
public int EducationStage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 省份
|
||||
/// </summary>
|
||||
public string Province { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 城市
|
||||
/// </summary>
|
||||
public string City { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 区县
|
||||
/// </summary>
|
||||
public string District { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 测评完成时间
|
||||
/// </summary>
|
||||
public DateTime? CompleteTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类测评结果数据
|
||||
/// </summary>
|
||||
public class CategoryResultDataDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类ID
|
||||
/// </summary>
|
||||
public long CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类名称
|
||||
/// </summary>
|
||||
public string CategoryName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 分类编码
|
||||
/// </summary>
|
||||
public string CategoryCode { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 分类类型(1-8)
|
||||
/// </summary>
|
||||
public int CategoryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父分类ID
|
||||
/// </summary>
|
||||
public long ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 得分
|
||||
/// </summary>
|
||||
public decimal Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 满分
|
||||
/// </summary>
|
||||
public decimal MaxScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 百分比
|
||||
/// </summary>
|
||||
public decimal Percentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排名
|
||||
/// </summary>
|
||||
public int Rank { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 星级(1-5)
|
||||
/// </summary>
|
||||
public int StarLevel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结论数据
|
||||
/// </summary>
|
||||
public class ConclusionDataDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类ID
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// 结论标题
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结论内容(富文本)
|
||||
/// </summary>
|
||||
public string Content { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类树节点
|
||||
/// </summary>
|
||||
public class CategoryTreeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 分类编码
|
||||
/// </summary>
|
||||
public string Code { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 分类类型(1-8)
|
||||
/// </summary>
|
||||
public int CategoryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父分类ID
|
||||
/// </summary>
|
||||
public long ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 子分类列表
|
||||
/// </summary>
|
||||
public List<CategoryTreeDto> Children { get; set; } = new();
|
||||
}
|
||||
144
temp_sql/batch18_report_web_pages.sql
Normal file
144
temp_sql/batch18_report_web_pages.sql
Normal file
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user