Files
EmailBill/openspec/changes/fix-budget-and-ui-bugs/design.md

144 lines
6.1 KiB
Markdown
Raw Normal View History

2026-02-15 10:10:28 +08:00
## Context
**当前状态**:
- 后端 `BudgetStatsService` 已正确计算 `Description` (HTML格式明细) 和 `Trend` (每日累计金额数组)
- Service 层的 `BudgetStatsDto` 包含这两个字段
- **问题**: Application 层在映射 DTO 时丢失了这两个字段,导致 API 响应不完整
- 前端使用 fallback 逻辑(线性估算)来弥补缺失数据,导致燃尽图显示为直线
**约束**:
- 修复必须向后兼容,不能破坏现有 API 契约
- 优先修复 Bug #4#5(高优先级),因为修复简单且影响大
- 前端已有完整的数据处理逻辑,只需后端提供正确数据
## Goals / Non-Goals
**Goals:**
- 修复 `BudgetStatsDetail` DTO 定义,添加 `Description``Trend` 字段
- 修复 `BudgetApplication.GetCategoryStatsAsync` 中的 DTO 映射逻辑
- 修复前端路由配置和 Vant 组件注册问题
- 分析并修复账单删除功能和金额不一致问题
- 添加单元测试覆盖修复场景
**Non-Goals:**
- 不重构 `BudgetStatsService` 的计算逻辑(已验证正确)
- 不改变前端图表组件的渲染逻辑(已有完整支持)
- 不修改 API 路由或版本化(向后兼容)
## Decisions
### 决策 1: 使用 `init` 关键字而非 `set` 来定义新字段
**理由**: `BudgetStatsDetail``record` 类型,遵循不可变对象模式。使用 `init` 确保字段只能在对象初始化时设置。
**替代方案**:
- 改用 `class` + `set` → 违背现有代码风格,且失去 record 的值语义
- 保持 `record` + `set` → C# 9+ 允许,但不符合不可变设计原则
### 决策 2: 在 Application 层映射时直接赋值,不做转换
**理由**: Service 层的 `BudgetStatsDto.Trend``Description` 已经是目标格式(`List<decimal?>``string`),无需额外处理。
**实现**:
```csharp
Month = new BudgetStatsDetail
{
Limit = stats.Month.Limit,
Current = stats.Month.Current,
Remaining = stats.Month.Remaining,
UsagePercentage = stats.Month.UsagePercentage,
Trend = stats.Month.Trend, // ⬅️ 新增
Description = stats.Month.Description // ⬅️ 新增
}
```
### 决策 3: Bug #1 (路由跳转) - 修改底部导航配置而非路由定义
**理由**: 经验证,`/statistics-v2` 路由已存在且正常工作。问题出在底部导航组件中硬编码了错误的路由路径。
**定位策略**:
1. 搜索底部导航组件代码 (通常包含 `van-tabbar``router-link`)
2. 检查"统计"标签的 `to` 属性或 `path` 配置
3. 修改为 `/statistics-v2`
### 决策 4: Bug #2 (删除功能) - 添加确认对话框而非直接删除
**理由**: 删除操作是破坏性的,应符合最佳实践要求用户确认。
**实现**:
```vue
const handleDelete = async () => {
const confirmed = await showConfirmDialog({
title: '确认删除',
message: '确定要删除这条账单吗?此操作无法撤销。'
})
if (confirmed) {
await deleteBill(billId)
closePopup()
}
}
```
### 决策 5: Bug #3 (组件警告) - 按需导入而非全局注册
**理由**: Vant 推荐使用按需导入,减少打包体积。全局注册可能是遗漏导致的警告。
**验证步骤**:
1. 检查 `main.ts` 或全局插件文件是否有 `DatetimePicker` 导入
2. 如果缺失,添加 `import { DatetimePicker } from 'vant'; app.use(DatetimePicker);`
3. 或在使用组件的文件中局部导入
### 决策 6: Bug #6 (金额不一致) - 先验证是否虚拟消耗导致
**理由**: Bug-handoff 文档指出硬性预算 (📌标记) 会产生虚拟消耗,这是设计行为而非 bug。
**验证逻辑**:
1. 检查不一致的预算是否标记为硬性预算
2. 检查 `BudgetService.GetPeriodRange` 返回的日期范围是否与 `periodStart/periodEnd` 一致
3. 如果是虚拟消耗:在前端账单列表中添加提示说明
4. 如果是日期范围问题:修复 `BudgetResult` 的赋值逻辑
## Risks / Trade-offs
### 风险 1: API 响应体积增大
**问题**: 新增 `Trend` 数组月度31个元素年度12个元素会增加响应大小。
**缓解措施**:
- `Trend` 数据是前端绘制图表必需的,体积增长合理
- 考虑后续添加 gzip 压缩到 API 响应(可选优化)
### 风险 2: 前端可能依赖旧的 fallback 逻辑
**问题**: 如果前端代码中有显式检查 `trend.length === 0` 的逻辑,可能会在修复后仍执行 fallback。
**缓解措施**:
- 在修复后端后,验证前端 `BudgetChartAnalysis.vue:603``629` 行的条件是否正确处理非空 trend
- 如果逻辑有问题,修改为 `if (!trend || trend.length === 0)`
### 风险 3: 测试覆盖不足可能导致回归
**问题**: 现有测试可能未覆盖 DTO 映射场景。
**缓解措施**:
-`WebApi.Test` 中添加针对 `BudgetApplication.GetCategoryStatsAsync` 的单元测试
- 验证返回的 DTO 包含非空的 `Description``Trend`
- 使用 `FluentAssertions` 编写清晰的断言
## Migration Plan
**部署步骤**:
1. 部署后端更新(向后兼容,前端可继续使用旧逻辑)
2. 验证 API 响应包含新字段 (使用 Swagger 或浏览器开发工具)
3. 前端无需额外部署(已支持新字段,会自动切换到真实数据)
4. 清除浏览器缓存以确保使用最新前端代码
**回滚策略**:
- 如果新版本出现问题,回滚到上一个 commit
- API 是向后兼容的(只添加字段),旧版前端仍可正常工作
**验证清单**:
- [ ] 预算明细弹窗显示完整的 HTML 表格
- [ ] 燃尽图显示波动曲线而非直线
- [ ] 底部导航"统计"按钮正常跳转
- [ ] 删除账单功能弹出确认对话框并正常工作
- [ ] 控制台无 `van-datetime-picker` 警告
- [ ] 金额不一致问题已分析并修复或说明
## Open Questions
1. **Bug #6 金额不一致的根本原因**: 需要在测试环境中验证是否为虚拟消耗导致,还是日期范围计算错误。
2. **前端 fallback 逻辑是否需要移除**: 当前 fallback 作为容错机制保留是否合理?还是应在有真实数据时完全禁用?
3. **是否需要添加 E2E 测试**: 当前只计划单元测试,是否需要添加端到端测试覆盖完整流程?