mi-assessment/.kiro/specs/admin-missing-modules/design.md
2026-02-08 11:31:08 +08:00

18 KiB
Raw Blame History

Design Document: 测评记录管理 & 业务介绍页内容管理

Overview

本设计文档描述了学业邑规划后台管理系统中两个遗漏模块的技术设计:

  1. 测评记录管理 — 基于已有的 assessment_recordsassessment_answersassessment_results 三张数据库表,提供测评记录的列表查询、详情查看、报告查看和数据导出功能。该模块为只读管理模块,不涉及数据的创建和修改。

  2. 业务介绍页内容管理 — 新增 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

设计决策

  1. 独立 Controller/Service — 测评记录管理创建独立的 AssessmentRecordControllerAssessmentRecordService,而非扩展现有的 AssessmentController/AssessmentService。原因:现有的 Assessment 模块已经很大1400+ 行),职责分离更利于维护。

  2. 只读模式 — 测评记录模块仅提供查询和导出功能,不提供创建/修改/删除接口。测评记录由小程序端 API 创建和更新。

  3. Excel 导出 — 使用 ClosedXML 库生成 Excel 文件,该库免费且无需安装 Office。

  4. 业务介绍页与 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 + MoqMock 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 往返一致