diff --git a/.kiro/specs/designated-prize-winner/design.md b/.kiro/specs/designated-prize-winner/design.md new file mode 100644 index 00000000..918b3dec --- /dev/null +++ b/.kiro/specs/designated-prize-winner/design.md @@ -0,0 +1,354 @@ +# Design Document: 指定用户中奖功能 + +## Overview + +本设计文档描述了"指定用户中奖"功能的技术实现方案。该功能允许后台管理员为盒子中的特定奖品指定中奖用户,使得该奖品只能被指定用户抽中(无限赏)或优先被指定用户抽中(有限赏带兜底)。 + +核心设计原则: +- 不修改奖品本身的概率配置 +- 有限赏保证"每抽必中"机制(兜底) +- 无限赏严格保护指定奖品 +- 最小化对现有抽奖流程的侵入 + +## Architecture + +```mermaid +graph TB + subgraph "后台管理层" + AdminAPI[后台管理 API] + DesignatedPrizeService[DesignatedPrizeService] + end + + subgraph "抽奖核心层" + LotteryEngine[LotteryEngine] + InventoryManager[InventoryManager] + PrizePoolFilter[奖品池过滤器] + end + + subgraph "数据层" + DbContext[HoneyBoxDbContext] + DesignatedPrizeEntity[GoodsDesignatedPrize] + GoodsItemEntity[GoodsItem] + end + + AdminAPI --> DesignatedPrizeService + DesignatedPrizeService --> DbContext + + LotteryEngine --> PrizePoolFilter + LotteryEngine --> InventoryManager + PrizePoolFilter --> DbContext + + DbContext --> DesignatedPrizeEntity + DbContext --> GoodsItemEntity +``` + +## Components and Interfaces + +### 1. 数据实体:GoodsDesignatedPrize + +```csharp +namespace HoneyBox.Model.Entities; + +/// +/// 指定中奖配置实体 +/// +public class GoodsDesignatedPrize +{ + public int Id { get; set; } + public int GoodsId { get; set; } + public int GoodsItemId { get; set; } + public int UserId { get; set; } + public bool IsActive { get; set; } = true; + public string? Remark { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + + // 导航属性 + public virtual Good? Goods { get; set; } + public virtual GoodsItem? GoodsItem { get; set; } + public virtual User? User { get; set; } +} +``` + +### 2. 抽奖引擎接口扩展:ILotteryEngine + +```csharp +public interface ILotteryEngine +{ + // 现有方法保持不变 + Task DrawAsync(LotteryDrawRequest request); + Task DrawInfiniteAsync(LotteryDrawRequest request); + + // 内部使用,不暴露新接口 +} +``` + +### 3. 指定中奖服务接口:IDesignatedPrizeService + +```csharp +namespace HoneyBox.Admin.Business.Services.Interfaces; + +public interface IDesignatedPrizeService +{ + Task> GetByGoodsIdAsync(int goodsId); + Task CreateAsync(CreateDesignatedPrizeRequest request); + Task UpdateAsync(int id, UpdateDesignatedPrizeRequest request); + Task DeleteAsync(int id); +} +``` + +### 4. 后台管理 API 控制器 + +```csharp +[ApiController] +[Route("api/admin/goods/{goodsId}/designated-prizes")] +public class DesignatedPrizeController : BusinessControllerBase +{ + [HttpGet] + public Task>> GetList(int goodsId); + + [HttpPost] + public Task> Create(int goodsId, CreateDesignatedPrizeRequest request); + + [HttpPut("{id}")] + public Task> Update(int goodsId, int id, UpdateDesignatedPrizeRequest request); + + [HttpDelete("{id}")] + public Task Delete(int goodsId, int id); +} +``` + +## Data Models + +### 数据库表设计 + +```sql +CREATE TABLE goods_designated_prizes ( + id INT PRIMARY KEY IDENTITY(1,1), + goods_id INT NOT NULL, + goods_item_id INT NOT NULL, + user_id INT NOT NULL, + is_active BIT NOT NULL DEFAULT 1, + remark NVARCHAR(200), + created_at DATETIME2 NOT NULL DEFAULT GETDATE(), + updated_at DATETIME2, + + INDEX IX_designated_goods_id (goods_id), + INDEX IX_designated_user_id (user_id), + INDEX IX_designated_goods_item (goods_item_id), + CONSTRAINT UQ_goods_item_user UNIQUE (goods_id, goods_item_id) +); +``` + +### DTO 模型 + +```csharp +// 查询响应 +public class DesignatedPrizeDto +{ + public int Id { get; set; } + public int GoodsId { get; set; } + public int GoodsItemId { get; set; } + public string? GoodsItemTitle { get; set; } + public string? GoodsItemImgUrl { get; set; } + public int UserId { get; set; } + public string? UserNickname { get; set; } + public string? UserPhone { get; set; } + public bool IsActive { get; set; } + public string? Remark { get; set; } + public DateTime CreatedAt { get; set; } +} + +// 创建请求 +public class CreateDesignatedPrizeRequest +{ + public int GoodsItemId { get; set; } + public int UserId { get; set; } + public string? Remark { get; set; } +} + +// 更新请求 +public class UpdateDesignatedPrizeRequest +{ + public int? UserId { get; set; } + public bool? IsActive { get; set; } + public string? Remark { get; set; } +} +``` + +### 抽奖引擎内部数据结构 + +```csharp +// 指定中奖配置缓存结构 +// Key: GoodsItemId, Value: UserId +private Dictionary _designatedPrizes; +``` + +## 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: Unique Constraint Enforcement +*For any* goods and goods_item combination, attempting to create a second designated prize configuration SHALL result in an error, ensuring only one user can be designated per prize. +**Validates: Requirements 1.2, 4.5** + +### Property 2: Active Configuration Filtering +*For any* query to get designated prize configurations, the result SHALL only contain configurations where is_active is true, regardless of how many inactive configurations exist. +**Validates: Requirements 1.3** + +### Property 3: Designated User Prize Pool Inclusion +*For any* designated user drawing from either finite or infinite lottery, their prize pool SHALL include all their designated prizes (where they are the designated user and is_active is true). +**Validates: Requirements 2.3, 3.4** + +### Property 4: Non-Designated User Prize Pool Exclusion (Finite Lottery) +*For any* non-designated user drawing from a finite lottery with normal prizes available, their prize pool SHALL NOT contain any designated prizes. +**Validates: Requirements 2.4** + +### Property 5: Non-Designated User Prize Pool Exclusion (Infinite Lottery - Strict) +*For any* non-designated user drawing from an infinite lottery, their prize pool SHALL NEVER contain designated prizes, even when no normal prizes exist. +**Validates: Requirements 3.3, 3.5** + +### Property 6: Finite Lottery Fallback Mechanism +*For any* finite lottery where only designated prizes remain (normal prize pool is empty), a non-designated user SHALL be able to draw from all remaining prizes (fallback mechanism). +**Validates: Requirements 2.5** + +### Property 7: Probability Calculation Invariance +*For any* prize pool (filtered or unfiltered), the probability calculation method SHALL remain unchanged - probabilities are calculated based on stock (finite) or real_pro (infinite) of the filtered pool, and the sum of probabilities SHALL equal 100%. +**Validates: Requirements 5.1, 5.3** + +### Property 8: Prize Data Immutability +*For any* designated prize configuration operation (create, update, delete), the underlying prize's stock, real_pro, and other probability-related fields SHALL remain unchanged. +**Validates: Requirements 5.2** + +### Property 9: Admin CRUD Validation +*For any* admin create operation, the system SHALL validate that goods_id, goods_item_id, and user_id all reference existing records; invalid references SHALL result in an error. +**Validates: Requirements 4.1** + +## Error Handling + +### 抽奖引擎错误处理 + +| 场景 | 处理方式 | 日志级别 | +|------|----------|----------| +| 指定中奖配置查询失败 | 降级为无配置模式,继续抽奖 | Warning | +| 有限赏兜底机制触发 | 记录日志,允许抽奖继续 | Warning | +| 无限赏奖品池为空(全是指定奖品) | 返回"暂无可抽奖品"错误 | Warning | +| 数据库连接异常 | 返回"系统繁忙"错误 | Error | + +### 后台管理 API 错误处理 + +| 场景 | HTTP 状态码 | 错误消息 | +|------|-------------|----------| +| 盒子不存在 | 404 | "盒子不存在" | +| 奖品不存在 | 404 | "奖品不存在" | +| 用户不存在 | 404 | "用户不存在" | +| 重复配置 | 400 | "该奖品已配置指定用户" | +| 配置不存在 | 404 | "配置不存在" | +| 权限不足 | 403 | "无权限操作" | + +### 错误处理代码示例 + +```csharp +// 抽奖引擎中的降级处理 +private async Task> GetDesignatedPrizesAsync(int goodsId) +{ + try + { + return await _dbContext.GoodsDesignatedPrizes + .Where(dp => dp.GoodsId == goodsId && dp.IsActive) + .ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to query designated prizes for goods {GoodsId}, falling back to no configuration", goodsId); + return new Dictionary(); + } +} +``` + +## Testing Strategy + +### 测试框架选择 + +- **单元测试框架**: xUnit +- **属性测试框架**: FsCheck (C# 属性测试库) +- **Mock 框架**: Moq +- **集成测试**: Microsoft.AspNetCore.Mvc.Testing + +### 单元测试覆盖 + +1. **GoodsDesignatedPrize 实体测试** + - 字段默认值验证 + - 导航属性关联 + +2. **DesignatedPrizeService 测试** + - CRUD 操作正确性 + - 验证逻辑(存在性检查) + - 唯一约束处理 + +3. **LotteryEngine 奖品池过滤测试** + - 有限赏过滤逻辑(带兜底) + - 无限赏过滤逻辑(严格) + - 边界情况(空池、全指定等) + +### 属性测试配置 + +每个属性测试运行 **100 次迭代**,使用 FsCheck 生成随机测试数据。 + +```csharp +// 属性测试示例配置 +[Property(MaxTest = 100)] +public Property DesignatedUserSeesTheirPrizes() +{ + // Feature: designated-prize-winner, Property 3: Designated User Prize Pool Inclusion + return Prop.ForAll( + Arb.From(), // userId + Arb.From>(), // prizePool + Arb.From>(), // designatedPrizes + (userId, prizePool, designatedPrizes) => + { + // Test implementation + }); +} +``` + +### 测试数据生成策略 + +```csharp +// 奖品池生成器 +public static Arbitrary> GoodsItemListArbitrary() +{ + return Gen.ListOf(Gen.Fresh(() => new GoodsItem + { + Id = Gen.Choose(1, 1000).Sample(0, 1).First(), + SurplusStock = Gen.Choose(0, 100).Sample(0, 1).First(), + RealPro = Gen.Choose(0, 100).Sample(0, 1).First() + })).ToArbitrary(); +} + +// 指定中奖配置生成器 +public static Arbitrary> DesignatedPrizesArbitrary() +{ + return Gen.DictionaryOf( + Gen.Choose(1, 1000), // GoodsItemId + Gen.Choose(1, 10000) // UserId + ).ToArbitrary(); +} +``` + +### 集成测试场景 + +1. **有限赏完整流程测试** + - 指定用户抽中指定奖品 + - 普通用户抽不到指定奖品 + - 兜底机制触发场景 + +2. **无限赏完整流程测试** + - 指定用户抽中指定奖品 + - 普通用户永远抽不到指定奖品 + +3. **后台管理 API 测试** + - 完整 CRUD 流程 + - 并发创建重复配置 + - 权限验证 + diff --git a/.kiro/specs/designated-prize-winner/requirements.md b/.kiro/specs/designated-prize-winner/requirements.md new file mode 100644 index 00000000..ef22435d --- /dev/null +++ b/.kiro/specs/designated-prize-winner/requirements.md @@ -0,0 +1,76 @@ +# Requirements Document + +## Introduction + +指定用户中奖功能允许后台管理员为每个盒子配置"指定用户中奖"规则,使某个奖品只能由指定用户抽中。该功能不修改奖品本身的概率,指定用户仍需按概率抽奖,但非指定用户无法抽中该奖品。 + +## Glossary + +- **Goods**: 盒子/商品,包含多个奖品的抽奖容器 +- **GoodsItem**: 奖品,盒子中的单个可抽取物品 +- **DesignatedPrize**: 指定中奖配置,将特定奖品指定给特定用户 +- **LotteryEngine**: 抽奖引擎,负责执行抽奖逻辑的核心服务 +- **FiniteLottery**: 有限赏抽奖,基于库存概率的抽奖模式(一番赏、福袋等),抽完即止 +- **InfiniteLottery**: 无限赏抽奖,基于固定概率的抽奖模式(无限赏、领主赏等),无限池 +- **PrizePool**: 奖品池,当前可抽取的奖品集合 +- **FallbackMechanism**: 兜底机制,当普通奖品池为空时允许普通用户抽取指定奖品 + +## Requirements + +### Requirement 1: 指定中奖配置数据管理 + +**User Story:** As a 后台管理员, I want to 为盒子中的奖品配置指定中奖用户, so that 特定奖品只能被指定用户抽中。 + +#### Acceptance Criteria + +1. THE GoodsDesignatedPrize Entity SHALL store goods_id, goods_item_id, user_id, is_active, remark, created_at, and updated_at fields +2. WHEN a designated prize configuration is created, THE System SHALL enforce that one prize can only be designated to one user per goods (unique constraint on goods_id + goods_item_id) +3. WHEN a designated prize configuration is queried, THE System SHALL only return active configurations (is_active = true) +4. THE System SHALL support enabling and disabling designated prize configurations via the is_active field + +### Requirement 2: 有限赏抽奖流程改造(带兜底机制) + +**User Story:** As a 用户, I want to 在有限赏抽奖时遵循指定中奖规则, so that 指定奖品优先给指定用户,但保证每抽必中。 + +#### Acceptance Criteria + +1. WHEN a user draws from a finite lottery, THE LotteryEngine SHALL query designated prize configurations for the goods +2. WHEN designated prize configurations exist, THE LotteryEngine SHALL separate the prize pool into normal prizes and designated prizes +3. WHEN the current user is a designated user for any prize, THE LotteryEngine SHALL include that user's designated prizes in their prize pool +4. WHEN the current user is not a designated user, THE LotteryEngine SHALL exclude designated prizes from their prize pool +5. IF the normal prize pool is empty (only designated prizes remain), THEN THE LotteryEngine SHALL allow the user to draw from all prizes (fallback mechanism) +6. WHEN the fallback mechanism is triggered, THE LotteryEngine SHALL log a warning for operational tracking + +### Requirement 3: 无限赏抽奖流程改造(严格过滤) + +**User Story:** As a 用户, I want to 在无限赏抽奖时遵循指定中奖规则, so that 指定奖品只能被指定用户抽中。 + +#### Acceptance Criteria + +1. WHEN a user draws from an infinite lottery, THE LotteryEngine SHALL query designated prize configurations for the goods +2. WHEN designated prize configurations exist, THE LotteryEngine SHALL strictly filter the prize pool +3. WHEN a prize has a designated user configuration and the current user is not the designated user, THE LotteryEngine SHALL remove that prize from the pool +4. WHEN a prize has a designated user configuration and the current user is the designated user, THE LotteryEngine SHALL keep that prize in the pool +5. THE InfiniteLottery SHALL NOT have a fallback mechanism (designated prizes are strictly protected) + +### Requirement 4: 后台管理 API + +**User Story:** As a 后台管理员, I want to 通过 API 管理指定中奖配置, so that 我可以灵活配置和调整指定中奖规则。 + +#### Acceptance Criteria + +1. WHEN an admin creates a designated prize configuration, THE System SHALL validate that the goods_id, goods_item_id, and user_id exist +2. WHEN an admin queries designated prize configurations for a goods, THE System SHALL return all configurations with user information +3. WHEN an admin updates a designated prize configuration, THE System SHALL update the is_active, user_id, or remark fields +4. WHEN an admin deletes a designated prize configuration, THE System SHALL remove the configuration from the database +5. IF a duplicate configuration is attempted (same goods_id and goods_item_id), THEN THE System SHALL return an error message + +### Requirement 5: 概率保持不变 + +**User Story:** As a 产品经理, I want to 确保指定中奖不改变奖品概率, so that 抽奖公平性得到保证。 + +#### Acceptance Criteria + +1. WHEN calculating prize probabilities, THE LotteryEngine SHALL use the original probability calculation method +2. THE DesignatedPrize configuration SHALL NOT modify the prize's stock, real_pro, or any probability-related fields +3. WHEN a designated user draws, THE LotteryEngine SHALL calculate probabilities based on the filtered prize pool diff --git a/.kiro/specs/designated-prize-winner/tasks.md b/.kiro/specs/designated-prize-winner/tasks.md new file mode 100644 index 00000000..9179f850 --- /dev/null +++ b/.kiro/specs/designated-prize-winner/tasks.md @@ -0,0 +1,151 @@ +# Implementation Plan: 指定用户中奖功能 + +## Overview + +本实现计划将"指定用户中奖"功能分解为可执行的编码任务。实现顺序为:数据层 → 抽奖核心层 → 后台管理层,确保每个步骤都能独立验证。 + +## Tasks + +- [-] 1. 数据层实现 + - [ ] 1.1 创建 GoodsDesignatedPrize 实体类 + - 在 `HoneyBox.Model/Entities/` 目录创建 `GoodsDesignatedPrize.cs` + - 包含 Id, GoodsId, GoodsItemId, UserId, IsActive, Remark, CreatedAt, UpdatedAt 字段 + - 添加导航属性 Goods, GoodsItem, User + - _Requirements: 1.1_ + + - [ ] 1.2 更新 HoneyBoxDbContext + - 添加 `DbSet GoodsDesignatedPrizes` 属性 + - 在 `OnModelCreating` 中配置实体映射 + - 配置唯一约束 (goods_id, goods_item_id) + - 配置索引 (goods_id, user_id, goods_item_id) + - _Requirements: 1.1, 1.2_ + + - [ ] 1.3 创建数据库迁移 SQL 脚本 + - 在 `server/HoneyBox/scripts/` 目录创建 `create_goods_designated_prizes.sql` + - 包含建表语句、索引、唯一约束 + - _Requirements: 1.1, 1.2_ + +- [ ] 2. 抽奖引擎核心改造 + - [ ] 2.1 添加指定中奖配置查询方法 + - 在 `LotteryEngine.cs` 中添加 `GetDesignatedPrizesAsync(int goodsId)` 私有方法 + - 返回 `Dictionary` (GoodsItemId → UserId) + - 只查询 is_active = true 的配置 + - 添加异常降级处理(查询失败返回空字典) + - _Requirements: 2.1, 3.1, 1.3_ + + - [ ] 2.2 实现有限赏奖品池过滤方法(带兜底) + - 添加 `FilterPrizePoolWithFallback(prizePool, userId, designatedPrizes)` 私有方法 + - 分离普通奖品和指定奖品 + - 指定用户:返回普通奖品 + 自己的指定奖品 + - 普通用户:优先返回普通奖品池 + - 兜底:普通奖品池为空时返回全部奖品池 + - 兜底触发时记录 Warning 日志 + - _Requirements: 2.2, 2.3, 2.4, 2.5, 2.6_ + + - [ ] 2.3 编写有限赏过滤方法的属性测试 + - **Property 3: Designated User Prize Pool Inclusion** + - **Property 4: Non-Designated User Prize Pool Exclusion (Finite)** + - **Property 6: Finite Lottery Fallback Mechanism** + - **Validates: Requirements 2.3, 2.4, 2.5** + + - [ ] 2.4 实现无限赏奖品池过滤方法(严格) + - 添加 `FilterPrizePoolStrict(prizePool, userId, designatedPrizes)` 私有方法 + - 移除所有非当前用户的指定奖品 + - 保留当前用户的指定奖品 + - 无兜底机制 + - _Requirements: 3.2, 3.3, 3.4, 3.5_ + + - [ ] 2.5 编写无限赏过滤方法的属性测试 + - **Property 5: Non-Designated User Prize Pool Exclusion (Infinite - Strict)** + - **Validates: Requirements 3.3, 3.5** + + - [ ] 2.6 改造 DrawAsync 方法(有限赏) + - 在获取奖品池后调用 `GetDesignatedPrizesAsync` + - 如果有配置,调用 `FilterPrizePoolWithFallback` 过滤奖品池 + - 保持后续概率计算和抽奖逻辑不变 + - _Requirements: 2.1, 2.2, 5.1, 5.3_ + + - [ ] 2.7 改造 DrawInfiniteAsync 方法(无限赏) + - 在获取奖品池后调用 `GetDesignatedPrizesAsync` + - 如果有配置,调用 `FilterPrizePoolStrict` 过滤奖品池 + - 保持后续概率计算和抽奖逻辑不变 + - _Requirements: 3.1, 3.2, 5.1, 5.3_ + + - [ ] 2.8 编写概率计算不变性属性测试 + - **Property 7: Probability Calculation Invariance** + - **Validates: Requirements 5.1, 5.3** + +- [ ] 3. Checkpoint - 核心功能验证 + - 确保所有测试通过,如有问题请询问用户 + +- [ ] 4. 后台管理服务层实现 + - [ ] 4.1 创建 DTO 模型 + - 在 `HoneyBox.Admin.Business/Models/DesignatedPrize/` 目录创建: + - `DesignatedPrizeDto.cs` - 查询响应 + - `CreateDesignatedPrizeRequest.cs` - 创建请求 + - `UpdateDesignatedPrizeRequest.cs` - 更新请求 + - _Requirements: 4.2_ + + - [ ] 4.2 创建 IDesignatedPrizeService 接口 + - 在 `HoneyBox.Admin.Business/Services/Interfaces/` 目录创建接口 + - 定义 GetByGoodsIdAsync, CreateAsync, UpdateAsync, DeleteAsync 方法 + - _Requirements: 4.1, 4.2, 4.3, 4.4_ + + - [ ] 4.3 实现 DesignatedPrizeService 服务 + - 在 `HoneyBox.Admin.Business/Services/` 目录创建实现类 + - 实现 GetByGoodsIdAsync:查询配置列表,关联用户和奖品信息 + - 实现 CreateAsync:验证存在性,检查唯一约束,创建配置 + - 实现 UpdateAsync:更新 is_active, user_id, remark 字段 + - 实现 DeleteAsync:删除配置 + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ + + - [ ] 4.4 编写服务层单元测试 + - 测试 CRUD 操作正确性 + - 测试验证逻辑(存在性检查) + - 测试唯一约束处理 + - _Requirements: 4.1, 4.5_ + + - [ ] 4.5 编写唯一约束属性测试 + - **Property 1: Unique Constraint Enforcement** + - **Validates: Requirements 1.2, 4.5** + +- [ ] 5. 后台管理 API 层实现 + - [ ] 5.1 创建 DesignatedPrizeController 控制器 + - 在 `HoneyBox.Admin.Business/Controllers/` 目录创建控制器 + - 路由:`api/admin/goods/{goodsId}/designated-prizes` + - 实现 GET(列表)、POST(创建)、PUT(更新)、DELETE(删除)端点 + - 添加权限验证 + - _Requirements: 4.1, 4.2, 4.3, 4.4_ + + - [ ] 5.2 注册服务到依赖注入容器 + - 在 `ServiceCollectionExtensions.cs` 中注册 IDesignatedPrizeService + - _Requirements: 4.1_ + + - [ ] 5.3 编写 API 集成测试 + - 测试完整 CRUD 流程 + - 测试错误响应(404, 400) + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ + +- [ ] 6. Checkpoint - 后台管理功能验证 + - 确保所有测试通过,如有问题请询问用户 + +- [ ] 7. 集成测试和文档 + - [ ] 7.1 编写端到端集成测试 + - 有限赏完整流程:配置指定中奖 → 指定用户抽奖 → 验证结果 + - 无限赏完整流程:配置指定中奖 → 普通用户抽奖 → 验证无法抽中 + - 兜底机制测试:只剩指定奖品时普通用户抽奖 + - _Requirements: 2.3, 2.4, 2.5, 3.3, 3.4, 3.5_ + + - [ ] 7.2 编写奖品数据不可变性属性测试 + - **Property 8: Prize Data Immutability** + - **Validates: Requirements 5.2** + +- [ ] 8. Final Checkpoint - 完整功能验证 + - 确保所有测试通过,如有问题请询问用户 + +## Notes + +- 每个任务都引用了具体的需求条款以确保可追溯性 +- 检查点用于增量验证,确保每个阶段的正确性 +- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况 +- 所有测试任务均为必需,确保全面的测试覆盖