18 KiB
Design Document: 测评记录管理 & 业务介绍页内容管理
Overview
本设计文档描述了学业邑规划后台管理系统中两个遗漏模块的技术设计:
-
测评记录管理 — 基于已有的
assessment_records、assessment_answers、assessment_results三张数据库表,提供测评记录的列表查询、详情查看、报告查看和数据导出功能。该模块为只读管理模块,不涉及数据的创建和修改。 -
业务介绍页内容管理 — 新增
business_pages数据库表,提供业务介绍页的完整 CRUD 功能,包括状态管理和排序管理。该模块与现有的轮播图(Banner)模块配合,支持从首页轮播图跳转到业务介绍详情页。
两个模块均遵循现有项目的架构模式:Controller → Service → DbContext,使用 RPC 风格接口(仅 GET/POST),集成现有权限系统。
Architecture
graph LR
subgraph Controllers
ARC[AssessmentRecordController]
BPC[BusinessPageController]
end
subgraph Services
ARS[AssessmentRecordService]
BPS[BusinessPageService]
end
subgraph Interfaces
IARS[IAssessmentRecordService]
IBPS[IBusinessPageService]
end
subgraph Data
DB[(AdminBusinessDbContext)]
end
subgraph Entities
AR[AssessmentRecord]
AA[AssessmentAnswer]
ARES[AssessmentResult]
BP[BusinessPage]
end
ARC --> IARS
BPC --> IBPS
IARS -.-> ARS
IBPS -.-> BPS
ARS --> DB
BPS --> DB
DB --> AR
DB --> AA
DB --> ARES
DB --> BP
设计决策
-
独立 Controller/Service — 测评记录管理创建独立的
AssessmentRecordController和AssessmentRecordService,而非扩展现有的AssessmentController/AssessmentService。原因:现有的 Assessment 模块已经很大(1400+ 行),职责分离更利于维护。 -
只读模式 — 测评记录模块仅提供查询和导出功能,不提供创建/修改/删除接口。测评记录由小程序端 API 创建和更新。
-
Excel 导出 — 使用 ClosedXML 库生成 Excel 文件,该库免费且无需安装 Office。
-
业务介绍页与 Banner 关联 — Banner 表已有
LinkType=1(内部页面)和LinkUrl字段,可直接存储业务介绍页的 Id 作为跳转目标,无需修改 Banner 表结构。
Components and Interfaces
测评记录模块
AssessmentRecordController
路由前缀: /api/admin/assessmentRecord
| 接口 | 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|---|
| GetList | GET | /getList |
assessmentRecord:view | 分页查询测评记录列表 |
| GetDetail | GET | /getDetail?id={id} |
assessmentRecord:view | 获取测评记录详情(含答案和结果) |
| GetReport | GET | /getReport?id={id} |
assessmentRecord:view | 获取测评报告(含结论) |
| Export | GET | /export |
assessmentRecord:export | 导出测评记录为 Excel |
IAssessmentRecordService
public interface IAssessmentRecordService
{
/// <summary>
/// 获取测评记录列表
/// </summary>
Task<PagedResult<AssessmentRecordDto>> GetRecordListAsync(AssessmentRecordQueryRequest request);
/// <summary>
/// 获取测评记录详情
/// </summary>
Task<AssessmentRecordDetailDto> GetRecordDetailAsync(long id);
/// <summary>
/// 获取测评报告
/// </summary>
Task<AssessmentReportDto> GetRecordReportAsync(long id);
/// <summary>
/// 导出测评记录
/// </summary>
Task<byte[]> ExportRecordsAsync(AssessmentRecordQueryRequest request);
}
业务介绍页模块
BusinessPageController
路由前缀: /api/admin/businessPage
| 接口 | 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|---|
| GetList | GET | /getList |
businessPage:view | 分页查询业务介绍页列表 |
| GetDetail | GET | /getDetail?id={id} |
businessPage:view | 获取业务介绍页详情 |
| Create | POST | /create |
businessPage:create | 创建业务介绍页 |
| Update | POST | /update |
businessPage:update | 更新业务介绍页 |
| Delete | POST | /delete |
businessPage:delete | 删除业务介绍页(软删除) |
| UpdateStatus | POST | /updateStatus |
businessPage:update | 更新状态 |
| UpdateSort | POST | /updateSort |
businessPage:update | 更新排序 |
IBusinessPageService
public interface IBusinessPageService
{
/// <summary>
/// 获取业务介绍页列表
/// </summary>
Task<PagedResult<BusinessPageDto>> GetPageListAsync(BusinessPageQueryRequest request);
/// <summary>
/// 获取业务介绍页详情
/// </summary>
Task<BusinessPageDto> GetPageByIdAsync(long id);
/// <summary>
/// 创建业务介绍页
/// </summary>
Task<long> CreatePageAsync(CreateBusinessPageRequest request);
/// <summary>
/// 更新业务介绍页
/// </summary>
Task<bool> UpdatePageAsync(UpdateBusinessPageRequest request);
/// <summary>
/// 删除业务介绍页(软删除)
/// </summary>
Task<bool> DeletePageAsync(long id);
/// <summary>
/// 更新业务介绍页状态
/// </summary>
Task<bool> UpdatePageStatusAsync(long id, int status);
/// <summary>
/// 更新业务介绍页排序
/// </summary>
Task<bool> UpdatePageSortAsync(long id, int sort);
}
Data Models
Entity 实体类
AssessmentRecord
[Table("assessment_records")]
public class AssessmentRecord
{
[Key]
public long Id { get; set; }
public long UserId { get; set; }
public long OrderId { get; set; }
public long AssessmentTypeId { get; set; }
[Required] [MaxLength(50)]
public string Name { get; set; } = null!;
[Required] [MaxLength(20)]
public string Phone { get; set; } = null!;
public int Gender { get; set; }
public int Age { get; set; }
public int EducationStage { get; set; }
[Required] [MaxLength(50)]
public string Province { get; set; } = null!;
[Required] [MaxLength(50)]
public string City { get; set; } = null!;
[Required] [MaxLength(50)]
public string District { get; set; } = null!;
public int Status { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? SubmitTime { get; set; }
public DateTime? CompleteTime { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
public bool IsDeleted { get; set; }
}
AssessmentAnswer
[Table("assessment_answers")]
public class AssessmentAnswer
{
[Key]
public long Id { get; set; }
public long RecordId { get; set; }
public long QuestionId { get; set; }
public int QuestionNo { get; set; }
public int AnswerValue { get; set; }
public DateTime CreateTime { get; set; }
}
AssessmentResult
[Table("assessment_results")]
public class AssessmentResult
{
[Key]
public long Id { get; set; }
public long RecordId { get; set; }
public long CategoryId { 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 DateTime CreateTime { get; set; }
}
BusinessPage
[Table("business_pages")]
public class BusinessPage
{
[Key]
public long Id { get; set; }
[Required] [MaxLength(100)]
public string Title { get; set; } = null!;
[Required] [MaxLength(500)]
public string ImageUrl { get; set; } = null!;
public bool HasActionButton { get; set; }
[MaxLength(50)]
public string? ActionButtonText { get; set; }
[MaxLength(500)]
public string? ActionButtonLink { get; set; }
public int Sort { get; set; }
public int Status { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
public bool IsDeleted { get; set; }
}
DTO 模型
测评记录模块 DTOs
| DTO | 用途 |
|---|---|
| AssessmentRecordQueryRequest : PagedRequest | 列表查询参数(UserId, AssessmentTypeId, Status, StartDate, EndDate) |
| AssessmentRecordDto | 列表项(含 UserNickname, AssessmentTypeName, OrderNo, StatusName, GenderName, EducationStageName) |
| AssessmentRecordDetailDto | 详情(含 Answers: List<AnswerDetailDto>, Results: List<ResultDetailDto>) |
| AnswerDetailDto | 答案详情(QuestionNo, QuestionContent, AnswerValue) |
| ResultDetailDto | 结果详情(CategoryName, CategoryTypeName, Score, MaxScore, Percentage, Rank, StarLevel) |
| AssessmentReportDto | 报告(RecordInfo + ResultGroups: List<ReportCategoryGroup>) |
| ReportCategoryGroup | 报告分类组(CategoryTypeName, Items: List<ReportCategoryItem>) |
| ReportCategoryItem | 报告分类项(CategoryName, Score, MaxScore, Percentage, StarLevel, ConclusionContent) |
业务介绍页模块 DTOs
| DTO | 用途 |
|---|---|
| BusinessPageQueryRequest : PagedRequest | 列表查询参数(Title, Status) |
| BusinessPageDto | 列表项/详情(所有字段 + StatusName) |
| CreateBusinessPageRequest | 创建请求(Title, ImageUrl, HasActionButton, ActionButtonText, ActionButtonLink, Sort, Status) |
| UpdateBusinessPageRequest | 更新请求(Id + 同创建请求字段) |
错误码
| 错误码 | 常量名 | 说明 | 模块 |
|---|---|---|---|
| 3241 | AssessmentRecordNotFound | 测评记录不存在 | 测评记录 |
| 3242 | AssessmentReportNotReady | 测评报告尚未生成 | 测评记录 |
| 3243 | ExportDataTooLarge | 导出数据量过大 | 测评记录 |
| 3701 | BusinessPageNotFound | 业务介绍页不存在 | 业务介绍页 |
数据库迁移 SQL
-- 创建 business_pages 表
CREATE TABLE business_pages (
Id bigint IDENTITY(1,1) PRIMARY KEY,
Title nvarchar(100) NOT NULL,
ImageUrl nvarchar(500) NOT NULL,
HasActionButton bit NOT NULL DEFAULT 0,
ActionButtonText nvarchar(50) NULL,
ActionButtonLink nvarchar(500) NULL,
Sort int NOT NULL DEFAULT 0,
Status int NOT NULL DEFAULT 1,
CreateTime datetime2 NOT NULL DEFAULT GETDATE(),
UpdateTime datetime2 NOT NULL DEFAULT GETDATE(),
IsDeleted bit NOT NULL DEFAULT 0
);
-- 创建索引
CREATE INDEX IX_business_pages_status ON business_pages (Status);
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: 分页查询返回记录数不超过 PageSize
For any assessment record dataset and valid PagedRequest (Page ≥ 1, 1 ≤ PageSize ≤ 100), the number of items returned by GetRecordListAsync should be ≤ PageSize, and the items should be sorted by CreateTime descending.
Validates: Requirements 1.1
Property 2: 筛选条件一致性
For any assessment record dataset and any combination of filter values (UserId, AssessmentTypeId, Status, StartDate, EndDate), every record returned by GetRecordListAsync must satisfy all specified filter criteria simultaneously.
Validates: Requirements 1.2, 1.7
Property 3: 列表关联数据完整性
For any record returned in the list response, the fields UserNickname, AssessmentTypeName, and OrderNo must be non-null and non-empty when the associated entities exist.
Validates: Requirements 1.3
Property 4: 枚举显示名称映射正确性
For any valid Status value in {1,2,3,4}, EducationStage value in {1,2,3,4,5,6}, and Gender value in {1,2}, the corresponding display name (StatusName, EducationStageName, GenderName) must match the defined mapping dictionary.
Validates: Requirements 1.4, 1.5, 1.6
Property 5: 详情答案排序不变量
For any assessment record with associated answers, the answers returned by GetRecordDetailAsync must be sorted by QuestionNo in strictly ascending order, and each answer must include non-empty QuestionContent.
Validates: Requirements 2.2, 2.5
Property 6: 详情结果数据完整性
For any assessment record with associated results, each result returned by GetRecordDetailAsync must include non-null CategoryName, a valid Score ≤ MaxScore, a Percentage between 0 and 100, and a StarLevel between 1 and 5.
Validates: Requirements 2.1, 2.3
Property 7: 报告按分类类型分组
For any completed assessment record (Status=4), the report returned by GetRecordReportAsync must group all results by CategoryType, and every result in a group must have the same CategoryType value.
Validates: Requirements 3.1
Property 8: 报告结论关联正确性
For any result item in the report, the ConclusionContent must correspond to the conclusion matching the result's CategoryId and StarLevel.
Validates: Requirements 3.2, 3.4
Property 9: 导出与列表筛选一致性
For any filter combination, the set of record Ids returned by ExportRecordsAsync must equal the complete (non-paginated) set of record Ids that would be returned by GetRecordListAsync with the same filters.
Validates: Requirements 4.1
Property 10: 业务介绍页验证规则
For any CreateBusinessPageRequest or UpdateBusinessPageRequest, if Title is empty or ImageUrl is empty, the service must reject the request. Additionally, if HasActionButton is true and ActionButtonText or ActionButtonLink is empty, the service must reject the request.
Validates: Requirements 5.1, 5.2, 7.1, 7.2
Property 11: 业务介绍页创建默认值
For any valid CreateBusinessPageRequest, the created entity must have CreateTime and UpdateTime set to approximately the current time, Status set to the request value (default 1), and IsDeleted set to false.
Validates: Requirements 5.3, 5.4
Property 12: 业务介绍页列表排序
For any business page dataset, the list returned by GetPageListAsync must be sorted by Sort descending, then by CreateTime descending.
Validates: Requirements 6.1
Property 13: 业务介绍页列表筛选
For any business page dataset and filter combination (Title fuzzy match, Status), every page returned must satisfy all specified filter criteria. For Title filtering, the returned page's Title must contain the search string.
Validates: Requirements 6.2
Property 14: 业务介绍页软删除不变量
For any existing business page, after calling DeletePageAsync, the page's IsDeleted must be true, and the page must not appear in subsequent GetPageListAsync results.
Validates: Requirements 8.1
Property 15: 业务介绍页 CRUD 往返一致性
For any valid CreateBusinessPageRequest, creating a page and then reading it back via GetPageByIdAsync must return an entity with matching Title, ImageUrl, HasActionButton, ActionButtonText, ActionButtonLink, and Sort values.
Validates: Requirements 5.1, 6.3, 7.1
Error Handling
错误码定义
在 ErrorCodes 类中新增以下错误码:
| 错误码 | 常量名 | 说明 | 触发条件 |
|---|---|---|---|
| 3241 | AssessmentRecordNotFound | 测评记录不存在 | 查询的记录 Id 不存在或已软删除 |
| 3242 | AssessmentReportNotReady | 测评报告尚未生成 | 记录状态不是 4(已完成)时请求报告 |
| 3243 | ExportDataTooLarge | 导出数据量过大 | 导出结果集超过 10000 条 |
| 3701 | BusinessPageNotFound | 业务介绍页不存在 | 查询的页面 Id 不存在或已软删除 |
错误处理策略
- Controller 层统一 try-catch,捕获
BusinessException返回业务错误码,捕获其他异常返回SystemError (5000) - Service 层通过抛出
BusinessException传递业务错误 - 所有查询自动过滤
IsDeleted = true的记录 - 参数验证在 Controller 层和 Service 层双重校验
Testing Strategy
测试框架
- 单元测试: xUnit + Moq(Mock DbContext 和 ILogger)
- 属性测试: FsCheck(.NET 属性测试库)
- 每个属性测试最少运行 100 次迭代
单元测试覆盖
| 测试场景 | 类型 | 说明 |
|---|---|---|
| 记录不存在返回错误 | 边界 | 验证 Requirements 2.4 |
| 未完成记录请求报告返回错误 | 边界 | 验证 Requirements 3.3 |
| 导出超限返回错误 | 边界 | 验证 Requirements 4.3 |
| 导出生成有效 Excel | 示例 | 验证 Requirements 4.4 |
| 业务页不存在返回错误 | 边界 | 验证 Requirements 7.4, 8.3 |
| 状态更新为无效值被拒绝 | 边界 | 验证 Requirements 8.2 |
属性测试覆盖
每个属性测试必须以注释标注对应的设计属性编号:
/// <summary>
/// Feature: admin-missing-modules, Property 1: 分页查询返回记录数不超过 PageSize
/// **Validates: Requirements 1.1**
/// </summary>
[Property]
public Property PaginationReturnsCorrectCount() { ... }
属性测试与设计属性的映射:
| 属性测试 | 设计属性 | 验证内容 |
|---|---|---|
| PaginationReturnsCorrectCount | Property 1 | 分页返回数量 ≤ PageSize |
| FilteredRecordsMatchCriteria | Property 2 | 筛选结果满足所有条件 |
| ListRecordsHaveAssociatedData | Property 3 | 列表关联数据完整 |
| EnumDisplayNameMappingsCorrect | Property 4 | 枚举映射正确 |
| DetailAnswersSortedByQuestionNo | Property 5 | 答案按题号排序 |
| DetailResultsHaveCompleteData | Property 6 | 结果数据完整 |
| ReportGroupedByCategoryType | Property 7 | 报告按分类分组 |
| ReportConclusionsMatchStarLevel | Property 8 | 结论匹配星级 |
| ExportMatchesListFilters | Property 9 | 导出与列表一致 |
| BusinessPageValidationRejectsInvalid | Property 10 | 验证规则拒绝无效请求 |
| BusinessPageCreationDefaults | Property 11 | 创建默认值正确 |
| BusinessPageListSortOrder | Property 12 | 列表排序正确 |
| BusinessPageListFilterMatch | Property 13 | 列表筛选正确 |
| BusinessPageSoftDeleteInvariant | Property 14 | 软删除不变量 |
| BusinessPageCrudRoundTrip | Property 15 | CRUD 往返一致 |