Files
EmailBill/openspec/changes/fix-budget-and-ui-bugs/design.md
SunCheng a88556c784 fix
2026-02-15 10:10:28 +08:00

144 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 测试**: 当前只计划单元测试,是否需要添加端到端测试覆盖完整流程?