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
优化后的系统更加稳定、高效和易维护。
