Penny Lens 储蓄功能修复日志

2025年1月27日
4 分钟阅读
作者:Penny Lens Team

Savings 虚拟余额和历史查询修复日志

更新时间

2025-01-27

修复问题

问题1:历史记录查询不出数据

原因

  • 使用 createdAt 字段进行时间过滤,但周归档记录的主要时间字段应该是 endDateTimestamp
  • 使用普通查询而非聚合查询,性能较差

解决方案

  • 改用 endDateTimestamp 字段进行时间过滤(更符合业务逻辑)
  • 使用聚合查询(aggregate)提升查询性能
  • 添加错误处理和日志
  • 优化空结果处理

问题2:虚拟余额相关方法逻辑混乱

原因

  • addVirtualTransaction 方法设计不清晰
  • 调用方需要判断传入正数还是负数
  • 容易出错,代码可读性差

解决方案

  • 重构 addVirtualTransaction 方法
  • 统一约定:调用方始终传入正数
  • 方法内部根据 type 自动处理正负:
    • type="deposit":存储正数(增加余额)
    • type="withdraw":存储负数(减少余额)

代码改动详情

1. 优化历史记录查询 (history方法)

修改文件:src/services/savingsService.ts

改动前

// 根据周期过滤
if (period === "month") {
  const startOfMonth = dayjs().startOf("month").valueOf();
  where.createdAt = db.command.gte(startOfMonth);  // ❌ 使用错误的字段
}
 
const result = await this.paginateQuery<WeekArchive>(
  savingsWeekArchivesCollection,
  where,
  page,
  pageSize,
  [{ field: "createdAt", order: "desc" }],  // ❌ 普通查询
);

改动后

// 根据周期过滤(使用周结束时间作为过滤条件)
if (period === "month") {
  const startOfMonth = dayjs().startOf("month").valueOf();
  matchCondition.endDateTimestamp = db.command.gte(startOfMonth);  // ✅ 使用正确字段
}
 
// ✅ 使用聚合查询提升性能
const aggregateResult = await savingsWeekArchivesCollection
  .aggregate()
  .match(matchCondition)
  .sort({ endDateTimestamp: -1 })
  .skip(skip)
  .limit(pageSize)
  .project({
    _id: 0,
    weekId: 1,
    startDateTimestamp: 1,
    endDateTimestamp: 1,
    goalYuan: 1,
    actualSpentYuan: 1,
    savedYuan: 1,
    status: 1,
    savingPowerStats: 1,
  })
  .end();

优势

  • ✅ 使用正确的时间字段过滤
  • ✅ 聚合查询性能更好
  • ✅ 提前检查总数,避免无效查询
  • ✅ 添加错误处理和日志

2. 重构虚拟交易方法 (addVirtualTransaction)

修改文件:src/services/savingsService.ts

改动前

private async addVirtualTransaction(
  userId: string,
  weekId: string,
  type: "deposit" | "withdraw",
  amountYuan: number,  // ❌ 不明确应该传正数还是负数
  description: string,
): Promise<void> {
  await savingsVirtualTransactionsCollection.add({
    data: {
      userId,
      weekId,
      type,
      amountYuan,  // ❌ 直接保存,依赖调用方处理正负
      description,
      createdAt: Date.now(),
    },
  });
}
 
// 调用示例(混乱)
await this.addVirtualTransaction(userId, weekId, "deposit", savedYuan, "..."); // 传正数
await this.addVirtualTransaction(userId, weekId, "withdraw", -amount, "..."); // 传负数

改动后

/**
 * 添加虚拟交易
 * @param amountYuan 金额(始终传入正数,方法内部会根据type自动处理正负)
 */
private async addVirtualTransaction(
  userId: string,
  weekId: string,
  type: "deposit" | "withdraw",
  amountYuan: number,  // ✅ 明确约定:始终传入正数
  description: string,
): Promise<void> {
  // ✅ 确保金额为正数
  const absoluteAmount = Math.abs(amountYuan);
  
  // ✅ 根据类型决定存储的金额正负
  const storedAmount = type === "deposit" ? absoluteAmount : -absoluteAmount;
 
  await savingsVirtualTransactionsCollection.add({
    data: {
      userId,
      weekId,
      type,
      amountYuan: storedAmount,  // ✅ 统一由方法处理正负
      description,
      createdAt: Date.now(),
    },
  });
}
 
// 调用示例(统一)
await this.addVirtualTransaction(userId, weekId, "deposit", savedYuan, "..."); // ✅ 传正数
await this.addVirtualTransaction(userId, weekId, "withdraw", amount, "..."); // ✅ 传正数

优势

  • ✅ 接口语义清晰:金额始终是正数
  • ✅ 降低调用方出错概率
  • ✅ 代码可读性更好
  • ✅ 统一处理逻辑

3. 更新所有调用点

周归档虚拟额度处理

// ✅ 首次生成虚拟额度
await this.addVirtualTransaction(userId, weekId, "deposit", savedYuan, `周结算储蓄 - ${weekId}`);
 
// ✅ 调整虚拟额度(增加或减少)
const adjustmentType = difference > 0 ? "deposit" : "withdraw";
const adjustmentAmount = Math.abs(difference);
await this.addVirtualTransaction(userId, weekId, adjustmentType, adjustmentAmount, description);
 
// ✅ 清零虚拟额度
await this.addVirtualTransaction(userId, weekId, "withdraw", previousAmount, `周结算储蓄清零 - ${weekId}`);

用户提取虚拟余额

// ✅ 创建提取交易(传入正数,方法内部会转为负数)
await this.addVirtualTransaction(userId, weekId, "withdraw", amountYuan, reason);

性能优化

聚合查询性能提升

使用 MongoDB 的聚合管道(aggregate pipeline)相比普通查询的优势:

  1. 投影优化:只返回需要的字段,减少数据传输
  2. 索引利用:更好地利用数据库索引
  3. 管道优化:数据库可以优化整个管道的执行
  4. 内存效率:支持流式处理大数据集

建议的数据库索引

// savingsWeekArchives 集合
db.savingsWeekArchives.createIndex({ userId: 1, endDateTimestamp: -1 })

使用示例

查询历史记录

// 查询本月的周归档
{
  "action": "savings.history",
  "period": "month",
  "page": 1,
  "pageSize": 10
}
 
// 查询本年的周归档
{
  "action": "savings.history",
  "period": "year",
  "page": 1,
  "pageSize": 20
}
 
// 查询所有周归档
{
  "action": "savings.history",
  "period": "week",
  "page": 1,
  "pageSize": 10
}

提取虚拟余额

{
  "action": "savings.withdraw",
  "amountYuan": 100,
  "reason": "用于实际消费"
}

测试建议

1. 历史记录查询测试

  • ✅ 测试有归档记录时的查询
  • ✅ 测试无归档记录时的查询(应返回空列表)
  • ✅ 测试不同 period 参数的过滤效果
  • ✅ 测试分页功能

2. 虚拟余额测试

  • ✅ 测试周归档时自动生成虚拟额度
  • ✅ 测试多次归档时的额度调整
  • ✅ 测试超支时的额度清零
  • ✅ 测试用户主动提取虚拟余额
  • ✅ 测试余额不足时的提取失败

3. 边界情况测试

  • ✅ 测试空数据情况
  • ✅ 测试大数据量分页
  • ✅ 测试并发操作

影响范围

后端

  • ✅ 修复历史记录查询问题
  • ✅ 优化查询性能
  • ✅ 提升代码质量和可维护性
  • ✅ 完全向后兼容

前端

  • ✅ 无需改动
  • ✅ 历史记录接口返回结果不变
  • ✅ 虚拟余额逻辑对前端透明

总结

此次修复主要解决了两个核心问题:

  1. 历史记录查询问题

    • 使用正确的时间字段(endDateTimestamp)
    • 采用高性能聚合查询
    • 提升用户体验
  2. 虚拟余额逻辑优化

    • 统一金额处理约定(始终传入正数)
    • 降低代码复杂度
    • 减少潜在bug

优化后的系统更加稳定、高效和易维护。