Penny Lens 储蓄功能修复日志
2026年6月24日 · 4 分钟阅读 · ixNieStudio
详细介绍Penny Lens储蓄功能中虚拟余额和历史查询的修复过程,包括问题分析、解决方案和性能优化。
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)相比普通查询的优势:
- 投影优化:只返回需要的字段,减少数据传输
- 索引利用:更好地利用数据库索引
- 管道优化:数据库可以优化整个管道的执行
- 内存效率:支持流式处理大数据集
建议的数据库索引
// 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. 边界情况测试
- ✅ 测试空数据情况
- ✅ 测试大数据量分页
- ✅ 测试并发操作
影响范围
后端
- ✅ 修复历史记录查询问题
- ✅ 优化查询性能
- ✅ 提升代码质量和可维护性
- ✅ 完全向后兼容
前端
- ✅ 无需改动
- ✅ 历史记录接口返回结果不变
- ✅ 虚拟余额逻辑对前端透明
总结
此次修复主要解决了两个核心问题:
-
历史记录查询问题
- 使用正确的时间字段(endDateTimestamp)
- 采用高性能聚合查询
- 提升用户体验
-
虚拟余额逻辑优化
- 统一金额处理约定(始终传入正数)
- 降低代码复杂度
- 减少潜在bug
优化后的系统更加稳定、高效和易维护。