12 KiB
Context
EmailBill 项目在早期开发了 V1 版本的日历、统计、预算三大核心功能模块。随着业务迭代,V2 版本已经重构并稳定运行,提供了更优的用户体验和代码架构。当前代码库中同时维护 V1 和 V2 两套实现,导致:
- 代码冗余: 前端包含
CalendarView.vue,BudgetView.vue,statisticsV1/Index.vue等页面,后端包含多个仅服务于 V1 的 API 端点 - 维护成本: 任何共享组件或数据模型的变更需要同时考虑 V1 和 V2 的兼容性
- 认知负担: 新开发者需要理解两套架构,增加了上手难度
- 测试负担: 需要维护两套测试用例,且部分测试已失效
当前状态
- V2 版本已完成功能对齐,用户已逐步迁移
- V1 相关 API 端点部分已标记
[Obsolete](如TransactionRecordController.GetDailyStatisticsAsync) - 路由守卫中包含 V1/V2 版本切换逻辑
约束条件
- 不能影响 V2 功能: 必须保证 V2 页面完全正常运行
- 共享组件不能删除:
TransactionList,TransactionDetail,PopupContainer等组件被 V2 使用 - Repository 层不能动: V1 和 V2 共用相同的数据访问层
- 需要手动测试: 删除后必须打开 V2 页面验证功能完整性
利益相关者
- 开发团队: 减少维护负担,简化架构
- 测试团队: 减少测试用例数量
- 最终用户: 无感知(已迁移到 V2)
Goals / Non-Goals
Goals
- 完全移除 V1 代码: 删除所有 V1 专用的页面、API、Service 方法
- 保持 V2 功能完整: 确保删除操作不影响 V2 版本的任何功能
- 清理路由配置: 移除 V1 路由和版本切换逻辑
- 验证功能正常: 通过手动测试验证 V2 页面能正常打开和使用
Non-Goals
- 不修改 Repository 层: V2 继续使用现有的 Repository,不进行任何修改
- 不修改数据库: 不涉及数据迁移或表结构变更
- 不重构 V2 代码: 仅删除 V1 代码,不对 V2 进行任何优化或重构
- 不移除共享组件: 即使组件原本为 V1 开发,只要 V2 在用就保留
- 不更新文档: API 文档和用户文档的更新不在本次变更范围内
Decisions
Decision 1: 采用自底向上的删除顺序 (Backend → Frontend)
选择: 先删除后端 Service/Application → 再删除 Controller → 最后删除前端页面和路由
理由:
- 依赖方向: 前端依赖后端 API,先删除底层可以避免悬挂引用
- 验证便利: 删除后端接口后,前端调用会立即报 404,易于发现遗漏
- 回滚简单: 如果出问题,可以快速恢复后端接口,前端暂时保留也不影响
备选方案 (未选择):
- 自顶向下 (Frontend → Backend): 先删除前端页面,可能导致后端接口成为"僵尸代码"难以发现
- 同步删除: 一次性删除所有层,风险较高且难以定位问题
Decision 2: 通过代码搜索确认 V1 专用性
选择: 对每个待删除的方法/接口进行全局搜索,确认仅被 V1 页面调用
搜索关键字:
- 前端页面:
CalendarView.vue,BudgetView.vue,statisticsV1/Index.vue - API 方法:
GetDailyStatistics,GetUncoveredCategories,GetArchiveSummary,GetBalanceStatistics - 路由:
/calendar,/budget,name: 'statistics'
验证标准:
- 如果搜索结果仅出现在以上三个页面中 → 可以安全删除
- 如果出现在其他文件中 → 标记为"需保留"或"需进一步分析"
理由:
- 准确性: 避免误删 V2 依赖的代码
- 可追溯: 搜索结果可作为删除依据留存
Decision 3: 保留共享组件,即使其仅在 V1 中被直接使用
选择: 保留 TransactionList, TransactionDetail, PopupContainer, SmartClassifyButton 等组件
判断依据:
- 通过搜索确认这些组件在 V2 页面中有引用
- 即使组件内部包含 V1 特定逻辑(如全局事件监听),也不在本次变更中修改
理由:
- 最小化风险: 删除共享组件可能导致 V2 功能异常
- 职责分离: 组件优化应作为独立的重构任务,不在本次删除范围内
Decision 4: 移除全局事件监听 (条件性)
选择: 如果全局事件 (transaction-deleted, transactions-changed) 仅在 V1 页面中监听,则移除事件触发代码
验证流程:
- 搜索
window.addEventListener('transaction-deleted') - 确认监听器仅在
CalendarView.vue和statisticsV1/Index.vue中注册 - 搜索
window.dispatchEvent(new Event('transaction-deleted')) - 如果触发位置在共享组件中(如
TransactionDetail.vue),检查该组件是否被 V2 使用- 如果被 V2 使用 → 保留事件触发代码(即使 V1 删除后无人监听)
- 如果仅 V1 使用 → 删除事件触发代码
理由:
- 保守策略: 优先保证 V2 功能不受影响
- 技术债务: 遗留的无用事件触发代码可作为后续清理任务
Decision 5: 采用手动测试验证 V2 功能
选择: 删除完成后,手动打开以下 V2 页面并验证核心功能
测试清单:
- 统计 V2 (
/statistics-v2):- 月度统计数据正常加载
- 分类统计饼图正常渲染
- 日度统计折线图正常渲染
- 交易列表正常展示和滚动加载
- 预算 V2 (
/budget-v2):- 预算列表正常加载
- 分类统计(月度+年度)正常显示
- 预算创建/编辑/删除功能正常
- 存款预算导航和显示正常
- 日历 V2 (
/calendar-v2):- 日历视图正常渲染
- 日期选择和统计数据加载正常
- 交易详情查看和编辑正常
理由:
- V2 无单元测试覆盖: 当前项目主要测试集中在后端,前端缺少自动化测试
- 快速反馈: 手动测试可以在 10 分钟内完成所有关键路径验证
- 用户体验保证: 确保最终用户不会遇到功能异常
备选方案 (未选择):
- 仅依赖编译检查: 无法发现运行时错误(如路由配置错误)
- 编写自动化测试: 时间成本过高,且本次变更不涉及逻辑修改
Risks / Trade-offs
Risk 1: 误删 V2 依赖的代码
表现: 删除后端接口或前端方法,导致 V2 页面调用失败
缓解措施:
- 采用 Decision 2 的搜索验证流程,确保每个删除操作都经过确认
- 先删除后端 Service 层,观察前端是否有新的 404 错误
- 使用 Git 进行版本控制,支持快速回滚
残留影响:
- 如果 V2 通过动态路由或字符串拼接调用接口,静态搜索可能遗漏
Risk 2: 共享组件包含 V1 特定逻辑但未被清理
表现: 保留的组件中包含 V1 特定的条件判断或事件监听,成为"死代码"
缓解措施:
- 标记为 Non-Goal,不在本次清理范围内
- 在任务清单中添加"后续优化"任务,记录需要清理的组件列表
残留影响:
- 代码库中保留少量无用代码,但不影响功能正确性
Risk 3: 路由守卫中遗留 V1 相关逻辑
表现: 删除 V1 路由后,router/index.js 或 useVersionStore 中仍有版本切换逻辑
缓解措施:
- 在任务清单中明确包含"清理路由守卫"任务
- 搜索
isV2(),useVersionStore,router.push等关键字,逐一检查
残留影响:
- 可能导致用户访问不存在的路由时出现 404 错误
Risk 4: V2 页面功能异常但手动测试未覆盖
表现: 删除操作影响了 V2 的边缘功能(如智能分类、批量操作),但未在测试清单中
缓解措施:
- 优先测试 V2 的核心流程(查看、创建、编辑、删除)
- 如果发现问题,立即回滚对应的删除操作
- 记录测试结果,作为归档的一部分
残留影响:
- 边缘功能可能在生产环境中被用户发现,需要后续修复
Trade-off 1: 保留共享组件 vs 彻底清理
权衡: 为了保证 V2 功能稳定,选择保留所有共享组件,即使部分组件包含 V1 特定逻辑
代价:
- 代码库中保留部分冗余代码
- 后续维护时可能需要额外的上下文理解
收益:
- 大幅降低误删风险
- 缩短变更周期,快速上线
Trade-off 2: 自动化测试 vs 手动测试
权衡: 采用手动测试而非编写自动化测试用例
代价:
- 无法在 CI/CD 中自动验证 V2 功能
- 未来的变更可能再次破坏 V2 功能
收益:
- 节省测试编写时间(预计 2-3 小时)
- 适用于一次性删除任务,性价比高
Migration Plan
阶段 1: 准备工作 (Pre-deletion)
- 创建 feature 分支
feature/remove-v1-modules - 运行所有现有测试,确认当前状态正常
- 备份 V1 相关文件列表(用于回滚)
阶段 2: 后端删除 (Backend Removal)
-
Service 层删除:
- 删除
BudgetService.GetUncoveredCategoriesAsync - 删除
BudgetService.GetArchiveSummaryAsync - 删除
TransactionStatisticsService.GetBalanceStatisticsAsync - 编译验证,确认无编译错误
- 删除
-
Application 层删除:
- 删除
BudgetApplication.GetUncoveredCategoriesAsync - 删除
BudgetApplication.GetArchiveSummaryAsync - 删除
TransactionStatisticsApplication.GetBalanceStatisticsAsync - 编译验证
- 删除
-
Controller 层删除:
- 删除
TransactionRecordController.GetDailyStatisticsAsync - 删除
BudgetController.GetUncoveredCategoriesAsync - 删除
BudgetController.GetArchiveSummaryAsync - 删除
TransactionStatisticsController.GetBalanceStatisticsAsync - 编译验证
- 删除
-
运行后端测试:
dotnet test WebApi.Test/WebApi.Test.csproj- 移除失败的 V1 相关测试用例
- 确保其他测试通过
阶段 3: 前端删除 (Frontend Removal)
-
API 客户端清理:
- 清理
Web/src/api/transactionRecord.js中的GetDailyStatistics调用 - 清理
Web/src/api/budget.js中的GetUncoveredCategories,GetArchiveSummary - 清理
Web/src/api/statistics.js中的GetBalanceStatistics
- 清理
-
页面文件删除:
- 删除
Web/src/views/CalendarView.vue - 删除
Web/src/views/BudgetView.vue - 删除
Web/src/views/statisticsV1/Index.vue - 删除
Web/src/views/statisticsV1/目录
- 删除
-
路由配置清理:
- 删除
Web/src/router/index.js中的 V1 路由定义:/calendar/budget/(statisticsV1)
- 简化
useVersionStore中的版本切换逻辑 - 移除路由守卫中的 V1 相关判断
- 删除
-
编译验证:
cd Web && pnpm build
阶段 4: 手动测试验证 (Manual Testing)
按照 Decision 5 的测试清单,逐项验证 V2 功能:
- 启动开发服务器:
pnpm dev - 打开浏览器,访问 V2 页面
- 验证核心功能(数据加载、交互、导航)
- 记录测试结果
阶段 5: 代码审查和合并 (Code Review & Merge)
- 提交变更到远程分支
- 创建 Pull Request
- 代码审查关注点:
- 确认删除的代码不在 V2 中被引用
- 检查是否有遗漏的 V1 特定逻辑
- 合并到主分支
回滚策略
如果发现 V2 功能异常:
- 立即回滚:
git revert <commit-hash> - 定位问题: 使用
git diff找到引起问题的删除操作 - 部分恢复: 仅恢复必要的文件或方法
- 重新测试: 确认恢复后 V2 功能正常
Open Questions
Q1: 是否需要保留 [Obsolete] 标记的接口一段时间?
当前决策: 直接删除,因为 V2 已稳定运行
待确认: 是否有外部系统或移动端 APP 仍在调用这些接口?如果有,需要先通知相关方并提供迁移时间
Q2: 全局事件监听 (transaction-deleted) 在 V2 中是否还需要?
当前决策: 保留事件触发代码,即使 V1 删除后无人监听
待确认: V2 是否使用其他机制(如 Pinia store)来实现组件间通信?如果是,可以在后续任务中移除全局事件
Q3: 是否需要更新 API 文档 (Swagger/Scalar)?
当前决策: 标记为 Non-Goal,不在本次范围内
待确认: API 文档是否自动生成?如果是手动维护,需要添加任务来移除已删除接口的文档
Q4: V2 页面是否有完整的功能对齐?
当前假设: V2 已完全替代 V1 功能
待确认: 是否有用户或内部团队仍在使用 V1 特有的功能(如 GetUncoveredCategories)?如果有,需要先在 V2 中实现对应功能
设计文档完成。本文档为实施团队提供了清晰的技术决策依据和操作步骤,确保删除操作安全可控。