Savings功能优化日志

2025年1月28日
3 分钟阅读
作者:Penny Lens Team

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方法重构
    • 支持两种模式:
      1. 不传weekId:自动计算最近完整周
      2. 传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:00
Cron表达式: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,所以迁移不是强制的,但建议执行以提升性能。

📊 影响范围

后端

  • ✅ 完全向后兼容
  • ✅ 性能优化
  • ✅ 代码更健壮

前端

  • ⚠️ 建议更新调用方式(非必须)
  • ✅ 旧的调用方式仍然可用
  • ✅ 可以简化代码逻辑

📝 总结

此次优化主要解决了两个核心问题:

  1. 数据持久化稳定性:通过使用weekId代替精确时间戳匹配,避免了WeeklyGoal丢失问题
  2. 接口合理性:WeekArchive接口从”前端计算”改为”后端计算”,支持自动化和定时任务

优化后的系统更加稳定、易用和自动化。