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

6.1 KiB
Raw Blame History

Context

当前状态:

  • 后端 BudgetStatsService 已正确计算 Description (HTML格式明细) 和 Trend (每日累计金额数组)
  • Service 层的 BudgetStatsDto 包含这两个字段
  • 问题: Application 层在映射 DTO 时丢失了这两个字段,导致 API 响应不完整
  • 前端使用 fallback 逻辑(线性估算)来弥补缺失数据,导致燃尽图显示为直线

约束:

  • 修复必须向后兼容,不能破坏现有 API 契约
  • 优先修复 Bug #4 和 #5高优先级因为修复简单且影响大
  • 前端已有完整的数据处理逻辑,只需后端提供正确数据

Goals / Non-Goals

Goals:

  • 修复 BudgetStatsDetail DTO 定义,添加 DescriptionTrend 字段
  • 修复 BudgetApplication.GetCategoryStatsAsync 中的 DTO 映射逻辑
  • 修复前端路由配置和 Vant 组件注册问题
  • 分析并修复账单删除功能和金额不一致问题
  • 添加单元测试覆盖修复场景

Non-Goals:

  • 不重构 BudgetStatsService 的计算逻辑(已验证正确)
  • 不改变前端图表组件的渲染逻辑(已有完整支持)
  • 不修改 API 路由或版本化(向后兼容)

Decisions

决策 1: 使用 init 关键字而非 set 来定义新字段

理由: BudgetStatsDetailrecord 类型,遵循不可变对象模式。使用 init 确保字段只能在对象初始化时设置。

替代方案:

  • 改用 class + set → 违背现有代码风格,且失去 record 的值语义
  • 保持 record + set → C# 9+ 允许,但不符合不可变设计原则

决策 2: 在 Application 层映射时直接赋值,不做转换

理由: Service 层的 BudgetStatsDto.TrendDescription 已经是目标格式(List<decimal?>string),无需额外处理。

实现:

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-tabbarrouter-link)
  2. 检查"统计"标签的 to 属性或 path 配置
  3. 修改为 /statistics-v2

决策 4: Bug #2 (删除功能) - 添加确认对话框而非直接删除

理由: 删除操作是破坏性的,应符合最佳实践要求用户确认。

实现:

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:603629 行的条件是否正确处理非空 trend
  • 如果逻辑有问题,修改为 if (!trend || trend.length === 0)

风险 3: 测试覆盖不足可能导致回归

问题: 现有测试可能未覆盖 DTO 映射场景。

缓解措施:

  • WebApi.Test 中添加针对 BudgetApplication.GetCategoryStatsAsync 的单元测试
  • 验证返回的 DTO 包含非空的 DescriptionTrend
  • 使用 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 测试: 当前只计划单元测试,是否需要添加端到端测试覆盖完整流程?