Savings功能优化日志
📅 更新时间
2025-10-05
🐛 问题描述
问题1:WeeklyGoal丢失问题
- 现象:前端有时会丢失WeeklyGoal数据
- 原因:使用精确时间戳(weekStartTimestamp, weekEndTimestamp)作为查询条件,前端计算的时间戳可能有微小差异导致查询不到已存在的目标
问题2:WeekArchive实现不合理
- 现象:需要前端提供weekStartTimestamp和weekEndTimestamp参数
- 问题:
- 依赖前端计算时间参数,容易出错
- 没有周完整性检查,可能归档未完成的周
- 不支持定时任务批量处理
- 参数由前端提供而非从数据库计算
🚀 优化方案
1. WeeklyGoal数据模型优化
修改文件:src/types/savings.ts
- 改动:在WeeklyGoal接口添加weekId字段
export interface WeeklyGoal {
id: string;
userId: string;
weekId: string; // 新增:e.g. 2025-W34(作为主要查询条件)
weekStartTimestamp: number;
weekEndTimestamp: number;
goalYuan: number;
}修改文件:src/services/savingsService.ts
- goals方法:
- 生成weekId并保存到数据库
- 使用weekId作为主要查询条件而非时间戳
- getWeekGoal方法:
- 改用weekId查询,避免时间戳精度问题
- 自动创建时也保存weekId
优势
- ✅ 更稳定的数据持久化
- ✅ 避免时间戳精度导致的查询失败
- ✅ weekId更语义化,便于调试
2. WeekArchive接口重构
修改文件:src/types/savings.ts
- 改动:简化WeekArchiveRequest参数
export interface WeekArchiveRequest {
weekId?: string; // 可选:指定要归档的周ID,不提供则自动计算最近完整周
}新增工具函数:src/utils/savingCalculator.ts
- getLastCompletedWeekInfo():获取上一个完整周的信息
- 如果当前周未完成,返回上一周
- 如果当前周已完成,返回当前周
- getWeekInfoFromWeekId(weekId):根据weekId解析周信息
- 解析格式:YYYY-WXX (如 2025-W34)
- 计算该周的开始和结束时间戳
修改文件:src/services/savingsService.ts
- archiveWeek方法重构:
- 支持两种模式:
- 不传weekId:自动计算最近完整周
- 传weekId:归档指定周
- 添加周完整性检查:未完成的周不允许归档
- 所有参数从数据库计算(周目标、消费数据等)
- 不依赖前端提供任何计算参数
- 支持两种模式:
修改文件:src/controllers/SavingsController.ts
- archiveWeek方法:
- 更新参数验证逻辑
- 验证weekId格式(如果提供)
优势
- ✅ 简化前端调用
- ✅ 数据更准确(完全基于后端数据库)
- ✅ 支持定时任务批量处理
- ✅ 防止误操作归档未完成的周
- ✅ 更好的错误提示
3. 定时任务支持
修改文件:src/services/schedulerService.ts
- 新增方法:executeSavingsWeekArchiveTask()
- 获取所有用户
- 为每个用户执行周归档(自动模式)
- 记录成功和失败统计
- 详细错误日志
修改文件:src/controllers/SchedulerController.ts
- 新增方法:executeSavingsWeekArchiveTask()
- 封装定时任务调用
修改文件:src/routes.ts
- 新增路由:scheduler.executeSavingsWeekArchiveTask
- requireAuth: false(定时任务不需要认证)
定时配置建议
执行时间:每周一 00:05:00Cron表达式:5 0 * * 1说明:在周日结束后的周一凌晨执行,确保上周数据完整🔄 接口变化
接口兼容性
- ✅
savings.goals- 完全兼容,只是内部实现优化 - ⚠️
savings.archiveWeek- 参数变化,但向后兼容- 旧版本:必须提供weekStartTimestamp和weekEndTimestamp
- 新版本:只需要提供weekId(可选)
- 建议:前端更新为传weekId或不传参数
新增接口
scheduler.executeSavingsWeekArchiveTask- 定时任务接口
📋 使用示例
场景1:前端手动归档当前周(周结束后)
{
"action": "savings.archiveWeek"
}场景2:前端归档指定周
{
"action": "savings.archiveWeek",
"weekId": "2025-W39"
}场景3:定时任务自动归档
{
"action": "scheduler.executeSavingsWeekArchiveTask"
}🗄️ 数据库索引建议
为了优化查询性能,建议添加以下索引:
savingsGoals集合
// 复合索引:优化周目标查询
db.savingsGoals.createIndex({ userId: 1, weekId: 1 })
// 单字段索引:向后兼容旧数据查询
db.savingsGoals.createIndex({ weekStartTimestamp: 1 })savingsWeekArchives集合
// 复合索引:优化周归档查询
db.savingsWeekArchives.createIndex({ userId: 1, weekId: 1 })
// 复合索引:优化历史记录查询
db.savingsWeekArchives.createIndex({ userId: 1, createdAt: -1 })🔄 数据迁移说明
WeeklyGoal数据迁移
现有的WeeklyGoal记录需要添加weekId字段:
// 迁移脚本示例
const goals = await db.collection('savingsGoals').get();
for (const goal of goals) {
if (!goal.weekId) {
const weekId = generateWeekIdFromTimestamp(goal.weekStartTimestamp);
await db.collection('savingsGoals').doc(goal._id).update({
data: { weekId }
});
}
}注意:新代码已经考虑了向后兼容,getWeekGoal方法会在查询时自动生成weekId,所以迁移不是强制的,但建议执行以提升性能。
📊 影响范围
后端
- ✅ 完全向后兼容
- ✅ 性能优化
- ✅ 代码更健壮
前端
- ⚠️ 建议更新调用方式(非必须)
- ✅ 旧的调用方式仍然可用
- ✅ 可以简化代码逻辑
📝 总结
此次优化主要解决了两个核心问题:
- 数据持久化稳定性:通过使用weekId代替精确时间戳匹配,避免了WeeklyGoal丢失问题
- 接口合理性:WeekArchive接口从”前端计算”改为”后端计算”,支持自动化和定时任务
优化后的系统更加稳定、易用和自动化。
